Merge remote-tracking branch 'origin/master'

This commit is contained in:
MHW
2026-03-15 09:33:09 +08:00
23 changed files with 1435 additions and 190 deletions

View File

@@ -53,6 +53,12 @@ public class FireRuleController extends BaseController {
return success(ruleService.getCommPlatformComponentNames(scenarioId));
}
@GetMapping("/platforms/{scenarioId}")
@ApiOperation("获取通信组件的所有平台和组件")
public AjaxResult platforms(@PathVariable Integer scenarioId){
return success(ruleService.findPlatformComponents(scenarioId));
}
/**
* 根据平台id获取平台下所有组件
* @param platformId

View File

@@ -0,0 +1,56 @@
package com.solution.rule.domain;
/*
* 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 java.io.Serializable;
import java.util.List;
public class Platform implements Serializable {
private Integer id;
private String name;
private String description;
private List<PlatformComponent> components;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<PlatformComponent> getComponents() {
return components;
}
public void setComponents(List<PlatformComponent> components) {
this.components = components;
}
}

View File

@@ -1,5 +1,6 @@
package com.solution.rule.mapper;
import com.solution.rule.domain.Platform;
import com.solution.rule.domain.PlatformComponent;
import com.solution.rule.domain.vo.ComponentCountVO;
import com.solution.rule.domain.vo.PlatformComponentNamesVO;
@@ -39,4 +40,6 @@ public interface FireRuleMapper {
* @return
*/
List<PlatformComponent> getComponents(Integer platformId);
List<Platform> findPlatformComponents(Integer scenarioId);
}

View File

@@ -1,6 +1,7 @@
package com.solution.rule.service;
import com.solution.rule.domain.FireRuleExecuteDTO;
import com.solution.rule.domain.Platform;
import com.solution.rule.domain.PlatformComponent;
import com.solution.rule.domain.vo.PlatformComponentNamesVO;
import com.solution.rule.domain.vo.PlatformWeaponAggregateVO;
@@ -37,4 +38,6 @@ public interface FireRuleService {
* @return
*/
List<PlatformComponent> getComponents(Integer platformId);
List<Platform> findPlatformComponents(Integer scenarioId);
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.solution.common.constant.ExceptionConstants;
import com.solution.rule.domain.FireRuleExecuteDTO;
import com.solution.rule.domain.Platform;
import com.solution.rule.domain.PlatformComponent;
import com.solution.rule.domain.RuleParam;
import com.solution.rule.domain.dto.WeaponModelDTO;
@@ -19,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -164,4 +166,14 @@ public class FireRuleServiceImpl implements FireRuleService {
}
return componentCountVOS;
}
@Override
public List<Platform> findPlatformComponents(Integer scenarioId) {
List<Platform> platforms = ruleMapper.findPlatformComponents(scenarioId);
if(!CollUtil.isEmpty(platforms)){
return platforms;
}
return new ArrayList<>();
}
}

View File

@@ -69,4 +69,39 @@
</select>
<resultMap id="VPlatformComponentMap" type="com.solution.rule.domain.PlatformComponent">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="type" column="type"/>
<result property="description" column="description"/>
<result property="platformId" column="platform_id"/>
<result property="num" column="num"/>
</resultMap>
<select id="findComponentsByPlatformId" resultMap="VPlatformComponentMap">
SELECT * FROM platform_component
WHERE platform_id=#{platformId}
</select>
<resultMap id="VPlatformMap" type="com.solution.rule.domain.Platform">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="description" column="description"/>
<collection property="components"
ofType="com.solution.rule.domain.PlatformComponent"
column="id"
select="findComponentsByPlatformId"/>
</resultMap>
<select id="findPlatformComponents" resultMap="VPlatformMap"
parameterType="java.lang.Integer">
SELECT
p.*,
pc.*
FROM platform p
LEFT JOIN platform_component pc ON p.id = pc.platform_id
WHERE pc.type = "comm"
AND p.scenario_id = #{scenarioId}
GROUP BY p.id;
</select>
</mapper>

View File

@@ -32,7 +32,15 @@ export const routes: RouteRecordRaw[] = [
meta: {
title: '决策树',
},
component: () => import('@/views/decision/designer.vue'),
component: () => import('@/views/decision/designer/designer.vue'),
},
{
name: 'decision-communication',
path: '/app/decision/communication',
meta: {
title: '通信',
},
component: () => import('@/views/decision/communication/communication.vue'),
},
{
name: 'decision-algorithm-management',
@@ -42,4 +50,12 @@ export const routes: RouteRecordRaw[] = [
},
component: () => import('@/views/decision/algorithm/management.vue'),
},
{
name: 'decision-fire-rule',
path: '/app/decision/fire-rule',
meta: {
title: '活力规则',
},
component: () => import('@/views/decision/rule/management.vue'),
},
]

View File

@@ -1647,4 +1647,184 @@
.anticon{
color: #eeeeee;
}
}
.ant-switch {
background: rgb(8 30 59);
}
.ks-algorithm-card {
.ant-card-head-title {
span.text {
display: block;
line-height: 30px;
}
}
}
.ks-sidebar-header {
line-height: 40px;
background: #081d36;
min-height: 40px;
background: url(@/assets/icons/card-head.png) left / 180% 100%;
padding: 0 10px;
.ks-sidebar-title {
color: #7ae8fc;
font-size: 16px;
.icon {
background: url(@/assets/icons/list.png) center / 100% 100%;
width: 25px;
height: 25px;
display: block;
margin-top: 7px;
}
.text{
margin-left: 40px;
font-size: 16px;
color: #eee;
}
}
.ks-sidebar-add {
position: absolute;
right: 7px;
top: 8px;
font-size: 12px;
.anticon {
display: block;
float: left;
line-height: 16px;
}
}
}
.ant-list {
&.ks-sidebar-list {
.ant-list-item {
cursor: pointer;
transition: all 0.5s;
border-left: 2px solid transparent;
position: relative;
&.selected,
&:hover {
background: #0a1b3c;
border-left: 2px solid #11377e;
}
}
.ks-sidebar-list-type {
position: absolute;
right: 10px;
.ant-badge {
.ant-badge-count {
color: #c3c2c2;
background: #333f7d;
box-shadow: 0 0 0 1px #325478;
}
}
}
.ant-list-item-meta {
.ant-list-item-meta-title {
color: #7ae8fc;
}
.ant-list-item-meta-description {
color: #4d8c98;
font-size: 13px;
}
}
}
}
.ks-sidebar-list-param-list {
padding: 15px;
border: 1px solid #475f71;
border-radius: 2px;
.ks-sidebar-list-param-item {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
}
.ks-sidebar-list-param-actions {
.anticon {
color: #7ae8fc;
font-size: 20px;
display: block;
line-height: 26px;
cursor: pointer;
}
}
.ant-collapse {
.ant-list-sm {
.ant-list-item {
padding: 4px 15px;
cursor: pointer;
color: rgb(130 196 233);
position: relative;
.ks-tree-actions {
position: absolute;
right: 10px;
display: none;
}
&:hover {
background: #0d2d4e;
.ks-tree-actions {
display: block;
}
}
}
}
&.ks-trees-collapse {
.ant-collapse-content-box {
padding: 0;
height: 40vh;
position: relative;
}
}
}
.create-tree-icon {
cursor: pointer;
}
.ant-list-item {
padding: 3px 5px;
cursor: pointer;
color: rgb(130 196 233);
&:hover {
background: #0d2d4e;
}
}
.ks-model-builder-body .ks-model-builder-left .ant-collapse {
&.platform-collapse{
.ant-collapse-content-box{
max-height: 45.5vh;
}
}
}

View File

@@ -11,13 +11,13 @@
新增
</a-button>
</div>
<a-list item-layout="horizontal" :data-source="algorithms" class="ks-algorithm-list">
<a-list item-layout="horizontal" :data-source="algorithms" class="ks-sidebar-list">
<template #renderItem="{ item }">
<a-list-item @click="()=> handleSelect(item)" :class="selectedAlgorithm?.id === item.id ? 'selected' : null">
<a-list-item-meta :description="substring(item.description,20)">
<template #title>
<span class="ks-algorithm-name">{{ substring(item.name, 20) }}</span>
<span class="ks-algorithm-type"><a-badge size="small" :count="getAlgorithmTypeName(item.type)"></a-badge></span>
<span class="ks-sidebar-list-type"><a-badge size="small" :count="getAlgorithmTypeName(item.type)"></a-badge></span>
</template>
</a-list-item-meta>
</a-list-item>
@@ -95,8 +95,8 @@
:name="['algorithmParamList']"
>
<a-form-item-rest>
<div class="ks-algorithm-param-list">
<div class="ks-algorithm-param-item" v-for="(item,index) in selectedAlgorithm.algorithmParamList">
<div class="ks-sidebar-list-param-list">
<div class="ks-sidebar-list-param-item" v-for="(item,index) in selectedAlgorithm.algorithmParamList">
<a-row :gutter="15">
<a-col :span="7">
<a-input v-model:value="item.paramName" placeholder="请输入参数名" />
@@ -108,7 +108,7 @@
<a-input v-model:value="item.description" placeholder="请输入描述" />
</a-col>
<a-col :span="3">
<a-space class="ks-algorithm-param-actions">
<a-space class="ks-sidebar-list-param-actions">
<MinusCircleOutlined @click="()=> handleMinus(index)" />
<PlusCircleOutlined @click="handleAdd" v-if="index === 0" />
</a-space>
@@ -125,8 +125,8 @@
:name="['algoConfig']"
>
<a-form-item-rest>
<div class="ks-algorithm-param-list">
<div class="ks-algorithm-param-item" v-for="(t,index) in selectedAlgorithm.algoConfigList">
<div class="ks-sidebar-list-param-list">
<div class="ks-sidebar-list-param-item" v-for="(t,index) in selectedAlgorithm.algoConfigList">
<a-row :gutter="15">
<a-col :span="7">
<a-select placeholder="请选择参数" v-model:value="t.name" @change="(v: any)=> t.name = v">
@@ -139,7 +139,7 @@
</a-select>
</a-col>
<a-col :span="3">
<a-space class="ks-algorithm-param-actions">
<a-space class="ks-sidebar-list-param-actions">
<MinusCircleOutlined @click="()=> handleMinusConfig(index)" />
<PlusCircleOutlined @click="handleAddConfig" v-if="index === 0" />
</a-space>
@@ -290,6 +290,7 @@ const handleCreate = () => {
const handleSelect = (item: Algorithm) => {
selectedAlgorithm.value = resolveItem(item);
formRef.value?.resetFields();
};
const handleAdd = () => {
@@ -394,120 +395,3 @@ const handleChange = (page: number, pageSize: number) => {
onMounted(() => load());
</script>
<style lang="less">
.ks-algorithm-card {
.ant-card-head-title {
span.text {
display: block;
line-height: 30px;
}
}
}
.ks-sidebar-header {
line-height: 40px;
padding: 5px 15px;
background: #081d36;
min-height: 40px;
background: url(@/assets/icons/card-head.png) left / 180% 100%;
padding: 0 10px;
.ks-sidebar-title {
color: #7ae8fc;
font-size: 16px;
.icon {
background: url(@/assets/icons/list.png) center / 100% 100%;
width: 25px;
height: 25px;
display: block;
margin-top: 7px;
}
.text{
margin-left: 40px;
font-size: 16px;
color: #eee;
}
}
.ks-sidebar-add {
position: absolute;
right: 7px;
top: 8px;
font-size: 12px;
.anticon {
display: block;
float: left;
line-height: 16px;
}
}
}
.ant-list {
&.ks-algorithm-list {
.ant-list-item {
cursor: pointer;
transition: all 0.5s;
border-left: 2px solid transparent;
position: relative;
&.selected,
&:hover {
background: #0a1b3c;
border-left: 2px solid #11377e;
}
}
.ks-algorithm-type {
position: absolute;
right: 10px;
.ant-badge {
.ant-badge-count {
color: #c3c2c2;
background: #333f7d;
box-shadow: 0 0 0 1px #325478;
}
}
}
.ant-list-item-meta {
.ant-list-item-meta-title {
color: #7ae8fc;
}
.ant-list-item-meta-description {
color: #4d8c98;
font-size: 13px;
}
}
}
}
.ks-algorithm-param-list {
padding: 15px;
border: 1px solid #475f71;
border-radius: 2px;
.ks-algorithm-param-item {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
}
.ks-algorithm-param-actions {
.anticon {
color: #7ae8fc;
font-size: 20px;
display: block;
line-height: 26px;
cursor: pointer;
}
}
</style>

View 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 { HttpRequestClient } from '@/utils/request';
import type { PlatformWithComponentsResponse, ScenarioPageableResponse, ScenarioRequest } from './types';
import type { BasicResponse } from '@/types';
const req = HttpRequestClient.create<BasicResponse>({
baseURL: '/api',
});
export const findScenarioByQuery = (_query: Partial<ScenarioRequest> = {}): Promise<ScenarioPageableResponse> => {
return new Promise((resolve) => {
resolve({
code: 200,
msg: null,
total: 1,
rows: [
{
id: 1,
name: '空战场景',
description: null,
},
],
});
});
};
export const deleteOneScenarioById = (id: number): Promise<BasicResponse> => {
return req.delete(`/system/behaviortree/${id}`);
};
export const findPlatformWithComponents = (id: number): Promise<PlatformWithComponentsResponse> => {
return req.get(`system/firerule/platforms/${id}`);
};

View File

@@ -0,0 +1,431 @@
<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">
<PlatformCard
ref="treesCardRef"
@create-tree="handleCreateTree"
@select-tree="handleSelectTree"
/>
<NodesCard
@drag-item-start="handleDragStart"
@drag-item-end="handleDragEnd"
/>
</div>
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
<div class="ks-model-builder-actions">
<a-space>
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" size="small" @click="handleSave">
<CheckOutlined />
<span>保存</span>
</a-button>
</a-space>
</div>
<!-- 画布容器添加拖放事件 -->
<div
ref="canvas"
class="ks-model-builder-canvas"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
@drop="handleDrop"
@dragover.prevent
></div>
<TeleportContainer />
</div>
</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 type { currentScenario, NodeDragTemplate, NodeTemplate } from '../types';
import type { GraphTaskElement, NodeGraph } from '../builder/element';
import { useGraphCanvas } from '../builder/hooks';
import { registerNodeElement } from '../builder/register';
import { createLineOptions } from '../builder/line';
import { createTree, findOneTreeById, updateTree } from '../designer/api';
import { createGraphTaskElement, hasElements, hasRootElementNode, resolveNodeGraph } from '../builder/utils';
import { createGraphTaskElementFromTemplate } from '../utils/node';
import PlatformCard from './platform-card.vue';
import NodesCard from './nodes-card.vue';
const TeleportContainer = defineComponent(getTeleport());
registerNodeElement();
export default defineComponent({
components: {
PlatformCard,
NodesCard,
Wrapper,
Header,
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<NodeDragTemplate | null>(null);
const isDraggingOver = ref(false);
const currentTreeEditing = ref<boolean>(false);
const currentScenario = ref<currentScenario | null>(null);
const currentNodeGraph = ref<NodeGraph | null>(null);
const selectedModelNode = ref<Node<NodeProperties> | null>(null);
const selectedNodeTaskElement = ref<GraphTaskElement | null>(null);
const changed = ref<boolean>(false);
const treesCardRef = ref<InstanceType<typeof PlatformCard> | null>(null);
const {
handleGraphEvent,
createCanvas,
zoomIn,
zoomOut,
fitToScreen,
centerContent,
resizeCanvas,
} = useGraphCanvas();
// 处理拖动开始
const handleDragStart = (nm: NodeDragTemplate) => {
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;
currentTreeEditing.value = false;
if (!currentScenario.value) {
message.error('请先选择或者创建行为树.');
return;
}
if (!graph.value || !canvas.value || !draggedNodeData.value) {
message.error('无法放置节点,缺少必要数据');
return;
}
try {
// 获取拖动的数据
const template = draggedNodeData.value as NodeDragTemplate;
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: GraphTaskElement = createGraphTaskElementFromTemplate(template, { x, y });
// 创建节点
const settingTaskNode = createGraphTaskElement(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 handleSelectTree = (tree: currentScenario) => {
console.info('handleSelectTree', tree);
findOneTreeById(tree.id).then(r => {
if (r.data) {
let nodeGraph: NodeGraph | null = null;
try {
nodeGraph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as NodeGraph;
} catch (e: any) {
console.error('parse error,cause:', e);
}
if (!nodeGraph) {
nodeGraph = {
nodes: [],
edges: [],
};
}
currentScenario.value = {
...r.data,
graph: nodeGraph,
};
currentTreeEditing.value = true;
createElements();
} else {
message.error(r.msg ?? '行为树不存在.');
}
});
};
const createElements = () => {
nextTick(() => {
try {
graph.value?.clearCells();
} catch (e: any) {
console.error('clear cells error, cause:', e);
}
setTimeout(() => {
if (currentScenario.value?.graph && graph.value) {
if (currentScenario.value?.graph.nodes) {
currentScenario.value?.graph.nodes.forEach(ele => {
const node = createGraphTaskElement(ele as GraphTaskElement);
console.info('create node: ', ele);
// 将节点添加到画布
graph.value?.addNode(node as Node);
});
}
if (currentScenario.value?.graph.edges) {
// 然后添加所有边,确保包含桩点信息
setTimeout(() => {
currentScenario.value?.graph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
});
}, 100); // 延迟一会儿,免得连线错位
}
}
}, 100);
});
};
const handleCreateTree = () => {
currentScenario.value = {
id: 0,
name: '行为树',
description: null,
englishName: null,
xmlContent: null,
createdAt: null,
graph: {
edges: [],
nodes: [],
},
updatedAt: null,
};
currentNodeGraph.value = {
edges: [],
nodes: [],
};
selectedModelNode.value = null;
selectedNodeTaskElement.value = null;
createElements();
};
// 初始化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('blank:click', () => {
selectedModelNode.value = null;
selectedNodeTaskElement.value = null;
currentTreeEditing.value = null !== currentScenario.value;
});
handleGraphEvent('node:click', (args: any) => {
const node = args.node as Node<NodeProperties>;
const newElement = node.getData() as GraphTaskElement;
selectedModelNode.value = node;
selectedNodeTaskElement.value = JSON.parse(JSON.stringify(newElement || {})) as GraphTaskElement;
});
// 监听节点鼠标事件,显示/隐藏连接点
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: GraphTaskElement) => {
// 更新选中的节点数据
if (selectedModelNode.value) {
selectedModelNode.value.replaceData(element);
}
console.info('handleUpdateElement', element);
// 更新本地引用
selectedNodeTaskElement.value = element;
changed.value = true;
};
const handleSave = () => {
const graphData: NodeGraph = resolveNodeGraph(graph.value as Graph);
console.info('handleSave', graphData);
if (!currentScenario.value) {
message.error('当前决策树不存在');
return;
}
const newTree: currentScenario = {
...currentScenario.value,
graph: graphData,
xmlContent: JSON.stringify(graphData),
};
if (!newTree.name) {
message.error('行为树名称不能为空.');
return;
}
if (!newTree.englishName) {
message.error('行为树英文名称不能为空.');
return;
}
let res = null;
if (currentScenario.value.id > 0) {
res = createTree(newTree);
} else {
res = updateTree(newTree);
}
res.then(r => {
if (r.code === 200) {
treesCardRef.value?.refresh();
message.success(r.msg ?? '操作成功.');
} else {
message.error(r.msg ?? '操作失败.');
}
});
};
// 初始化
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 {
treesCardRef,
handleCreateTree,
currentTreeEditing,
currentScenario,
currentNodeGraph,
selectedNodeTaskElement,
selectedModelNode,
graph,
canvas,
zoomIn,
zoomOut,
fitToScreen,
centerContent,
handleDragStart,
handleDragEnd,
handleDragEnter,
handleDragLeave,
handleDrop,
isDraggingOver,
handleSave,
handleUpdateElement,
handleSelectTree,
};
},
});
</script>

View File

@@ -0,0 +1,108 @@
<template>
<div class="w-full">
<a-collapse v-model:activeKey="activeKey" :accordion="true" class="platform-collapse">
<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 templateData" :span="12">
<div
:key="nm.id"
class="ks-model-drag-item"
@dragend="handleDragEnd"
@dragstart="handleDragStart($event, nm)"
>
<img :alt="nm.description ?? nm.name ?? ''" class="icon" src="@/assets/icons/model-4.svg" />
<span class="desc">{{ nm.description ?? nm.name }}</span>
</div>
</a-col>
</a-row>
</div>
</a-collapse-panel>
</a-collapse>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
import {findPlatformWithComponents} from './api'
import {type PlatformWithComponents} from './types'
export default defineComponent({
emits: ['drag-item-start', 'drag-item-end'],
setup(_props, { emit }) {
const activeKey = ref<number>(1);
const templateData = ref<PlatformWithComponents[]>([]);
const isDraggingOver = ref<boolean>(false);
const draggedNodeData = ref<PlatformWithComponents | null>(null);
const loadTress = () => {
templateData.value = []
findPlatformWithComponents(1).then(r => {
templateData.value = r.data ?? []
});
};
const handleDragStart = (e: DragEvent, nm: PlatformWithComponents) => {
let dragNode: PlatformWithComponents = { ...nm };
draggedNodeData.value = dragNode as PlatformWithComponents;
if (e.dataTransfer) {
e.dataTransfer.setData('text/plain', JSON.stringify(draggedNodeData.value));
e.dataTransfer.effectAllowed = 'copyMove';
const dragPreview = document.createElement('div');
dragPreview.textContent = dragNode.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', dragNode, group, isDraggingOver.value, e);
console.log('开始拖动:', dragNode);
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);
};
const load = ()=> {
findPlatformWithComponents(1).then(re=> {
console.error(re);
})
}
onMounted(() => {
loadTress();
load();
});
return {
activeKey,
templateData,
handleDragStart,
handleDragEnd,
};
},
});
</script>

View File

@@ -0,0 +1,141 @@
<template>
<a-collapse v-model:activeKey="activeKey" :accordion="false" class="ks-trees-collapse">
<a-collapse-panel key="1" style="position: relative">
<template #header>
<a-flex>
<span class="ks-model-builder-title-icon icon-model"></span>
<span style="color: #82c4e9;font-size: 16px;">场景</span>
<!-- <span style="position: absolute; right: 15px;"><PlusOutlined @click="$emit('create-tree')"/></span>-->
</a-flex>
</template>
<div class="w-full" style="padding: 5px;">
<a-flex>
<a-input-search v-model:value="scenarioQuery.name" allowClear placeholder="场景名称" size="small" @search="loadTress" />
<!-- <a-button size="small" style="margin-left: 10px;">-->
<!-- <PlusOutlined style="margin-top: 0px;display: block;" @click="$emit('create-tree')" />-->
<!-- </a-button>-->
</a-flex>
</div>
<a-list :data-source="scenario || []" size="small" style="min-height: 25vh">
<template #renderItem="{ item }">
<a-list-item>
<a-flex>
<a-tooltip placement="bottom">
<template #title>
<p>名称: {{ item.name }}</p>
<p>说明: {{ item.description }}</p>
</template>
<span>{{ substring(item.name, 15) }}</span>
</a-tooltip>
<a-flex class="ks-tree-actions">
<span style="margin-right: 10px" @click="()=> handleSelect(item)"><EditFilled /></span>
<a-popconfirm
title="确定删除?"
@confirm="()=> handleDelete(item)"
>
<span class="ks-tree-action ks-tree-action-delete"><DeleteOutlined /></span>
</a-popconfirm>
</a-flex>
</a-flex>
</a-list-item>
</template>
</a-list>
<a-pagination
v-model:current="scenarioQuery.pageNum"
:page-size="scenarioQuery.pageSize"
:total="totalTress"
simple size="small" @change="handleChange" />
</a-collapse-panel>
</a-collapse>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { CheckOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue';
import type { Scenario, ScenarioRequest } from './types';
import { deleteOneScenarioById, findScenarioByQuery } from './api';
import { substring } from '@/utils/strings';
export default defineComponent({
emits: ['select-tree', 'create-tree'],
components: {
CheckOutlined,
PlusOutlined,
DeleteOutlined,
EditFilled,
},
setup(_props, { emit }) {
const scenario = ref<Scenario[]>([]);
const scenarioQuery = ref<Partial<ScenarioRequest>>({
name: null,
pageNum: 1,
pageSize: 8,
});
const activeKey = ref<number>(1);
const totalTress = ref<number>(0);
const loadTress = () => {
findScenarioByQuery(scenarioQuery.value).then(r => {
scenario.value = r.rows;
totalTress.value = r.total ?? 0;
});
};
const handleChange = (page: number, pageSize: number) => {
scenarioQuery.value.pageNum = page;
scenarioQuery.value.pageSize = pageSize;
loadTress();
};
const handleDelete = (item: Scenario) => {
deleteOneScenarioById(item.id).then(r => {
if (r.code === 200) {
loadTress();
}
});
};
const columns = [
{
title: '名称',
dataIndex: 'name',
},
];
const handleSelect = (record: Scenario) => {
emit('select', record);
};
const customRow = (record: Scenario) => {
return {
onClick: (event: any) => {
emit('select', record, event);
},
};
};
const refresh = () => loadTress();
onMounted(() => {
loadTress();
});
return {
refresh,
totalTress,
substring,
activeKey,
scenario,
scenarioQuery,
loadTress,
columns,
customRow,
handleSelect,
handleChange,
handleDelete,
};
},
});
</script>

View File

@@ -0,0 +1,49 @@
/*
* 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 { ApiDataResponse, NullableString, PageableResponse } from '@/types';
export interface Scenario {
id: number,
name: NullableString,
description: NullableString,
}
export interface ScenarioRequest extends Scenario {
pageNum: number,
pageSize: number,
}
export interface ScenarioPageableResponse extends PageableResponse<Scenario> {
}
export interface Platform {
id: number,
name: NullableString,
description: NullableString,
scenarioId: number,
}
export interface PlatformComponent {
id: number,
name: NullableString,
type: NullableString,
description: NullableString,
platformId: number,
}
export interface PlatformWithComponents extends Platform {
components: PlatformComponent[],
}
export interface PlatformWithComponentsResponse extends ApiDataResponse<PlatformWithComponents[]> {
}

View File

@@ -8,7 +8,7 @@
*/
import { HttpRequestClient } from '@/utils/request';
import type { BehaviorTree, BehaviorTreeDetailsResponse, BehaviorTreePageResponse, BehaviorTreeRequest, NodeTemplatesResponse } from './types';
import type { BehaviorTree, BehaviorTreeDetailsResponse, BehaviorTreePageResponse, BehaviorTreeRequest, NodeTemplatesResponse } from '../types';
import type { BasicResponse } from '@/types';
const req = HttpRequestClient.create<BasicResponse>({

View File

@@ -72,16 +72,16 @@ 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 Header from '../header.vue';
import Properties from './properties.vue';
import type { BehaviorTree, NodeDragTemplate, NodeTemplate } from './types';
import type { GraphTaskElement, NodeGraph } from './builder/element';
import { useGraphCanvas } from './builder/hooks';
import { registerNodeElement } from './builder/register';
import { createLineOptions } from './builder/line';
import type { BehaviorTree, NodeDragTemplate, NodeTemplate } from '../types';
import type { GraphTaskElement, NodeGraph } from '../builder/element';
import { useGraphCanvas } from '../builder/hooks';
import { registerNodeElement } from '../builder/register';
import { createLineOptions } from '../builder/line';
import { createTree, findOneTreeById, updateTree } from './api';
import { createGraphTaskElement, hasElements, hasRootElementNode, resolveNodeGraph } from './builder/utils';
import { createGraphTaskElementFromTemplate } from './utils/node';
import { createGraphTaskElement, hasElements, hasRootElementNode, resolveNodeGraph } from '../builder/utils';
import { createGraphTaskElementFromTemplate } from '../utils/node';
import TressCard from './trees-card.vue';
import NodesCard from './nodes-card.vue';

View File

@@ -71,7 +71,7 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import type { NodeDragTemplate, NodeTemplate } from './types';
import type { NodeDragTemplate, NodeTemplate } from '../types';
import { findNodeTemplates } from './api';
import { safePreventDefault, safeStopPropagation } from '@/utils/event';

View File

@@ -147,8 +147,8 @@
<script lang="ts">
import { defineComponent, onMounted, type PropType, ref, watch } from 'vue';
import { CheckOutlined } from '@ant-design/icons-vue';
import type { ElementVariable, GraphTaskElement } from './builder/element';
import type { BehaviorTree } from './types';
import type { ElementVariable, GraphTaskElement } from '../builder/element';
import type { BehaviorTree } from '../types';
import type { Graph, Node, NodeProperties } from '@antv/x6';
import { generateKey } from '@/utils/strings';

View File

@@ -52,7 +52,7 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { CheckOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue';
import type { BehaviorTree, BehaviorTreeRequest } from './types';
import type { BehaviorTree, BehaviorTreeRequest } from '../types';
import { deleteOneTreeById, findTreesByQuery } from './api';
import { substring } from '@/utils/strings';
@@ -139,54 +139,3 @@ export default defineComponent({
</script>
<style lang="less">
.ant-collapse {
.ant-list-sm {
.ant-list-item {
padding: 4px 15px;
cursor: pointer;
color: rgb(130 196 233);
position: relative;
.ks-tree-actions {
position: absolute;
right: 10px;
display: none;
}
&:hover {
background: #0d2d4e;
.ks-tree-actions {
display: block;
}
}
}
}
&.ks-trees-collapse {
.ant-collapse-content-box {
padding: 0;
height: 40vh;
position: relative;
}
}
}
.create-tree-icon {
cursor: pointer;
}
.ant-list-item {
padding: 3px 5px;
cursor: pointer;
color: rgb(130 196 233);
&:hover {
background: #0d2d4e;
}
}
</style>

View 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 { HttpRequestClient } from '@/utils/request';
import type { FireRule, FireRulePageableResponse, FireRuleRequest } from './types';
import type { BasicResponse } from '@/types';
const req = HttpRequestClient.create<BasicResponse>({
baseURL: '/api',
});
export const findFireRuleByQuery = (query: Partial<FireRuleRequest> = {}): Promise<FireRulePageableResponse> => {
return req.get('/system/rule/list', query);
};
export const createFireRule = (fireRule: FireRule): Promise<BasicResponse> => {
return req.postJson('/system/rule', fireRule);
};
export const updateFireRule = (fireRule: FireRule): Promise<BasicResponse> => {
return req.putJson('/system/rule', fireRule);
};
export const deleteFireRule = (id: number): Promise<BasicResponse> => {
return req.delete(`/system/rule/${id}`);
};

View File

@@ -0,0 +1,8 @@
/*
* 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.
*/

View File

@@ -0,0 +1,254 @@
<template>
<Layout>
<template #sidebar>
<div class="ks-sidebar-header">
<a-flex class="ks-sidebar-title">
<span class="icon"></span>
<span class="text">火力规则管理</span>
</a-flex>
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
<PlusOutlined />
新增
</a-button>
</div>
<a-list item-layout="horizontal" :data-source="datasource" class="ks-sidebar-list">
<template #renderItem="{ item }">
<a-list-item @click="()=> handleSelect(item)" :class="selectedFireRule?.id === item.id ? 'selected' : null">
<a-list-item-meta :description="substring(item.description,20)">
<template #title>
<span class="ks-algorithm-name">{{ substring(item.name, 20) }}</span>
<span class="ks-sidebar-list-type"><a-badge size="small" :count="getSceneTypeName(item)"></a-badge></span>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<a-pagination
v-model:current="query.pageNum"
:page-size="query.pageSize"
:total="datasourceTotal"
simple size="small" @change="handleChange" />
</template>
<div class="w-full h-full">
<a-card class="ks-page-card ks-algorithm-card">
<template #title>
<a-space>
<span class="point"></span>
<span class="text">规则配置</span>
</a-space>
</template>
<div class="ks-scrollable" style="height: 80.5vh;overflow-y: auto;padding-right: 10px">
<a-row :gutter="15">
<a-col :span="16">
<a-form
ref="formRef"
:label-col="{span: 6}"
:model="selectedFireRule"
autocomplete="off"
layout="horizontal"
name="basic"
>
<a-form-item
label="规则名称"
:rules="[{ required: true, message: '请输入规则名称!', trigger: ['input', 'change'] }]"
:name="['name']"
>
<a-input v-model:value="selectedFireRule.name" placeholder="请输入规则名称" />
</a-form-item>
<a-form-item
label="场景类型"
:name="['sceneType']"
>
<a-select v-model:value="selectedFireRule.sceneType" placeholder="请选择场景类型">
<a-select-option :value="null">通用</a-select-option>
<a-select-option :value="0">防御</a-select-option>
<a-select-option :value="1">空降</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="触发条件"
:rules="[{ required: true, message: '请输入触发条件!', trigger: ['input', 'change'] }]"
:name="['conditions']"
>
<a-input v-model:value="selectedFireRule.conditions" placeholder="请输入触发条件" />
</a-form-item>
<a-form-item
label="响应动作"
:rules="[{ required: true, message: '请输入响应动作!', trigger: ['input', 'change'] }]"
:name="['actions']"
>
<a-input v-model:value="selectedFireRule.actions" placeholder="请输入响应动作" />
</a-form-item>
<a-form-item
label="优先级(数值越小优先级越高)"
:rules="[{ required: true, message: '请输入优先级!', trigger: ['input', 'change'] }]"
:name="['priority']"
>
<a-input-number style="width:100%;" v-model:value="selectedFireRule.priority" placeholder="请输入优先级" />
</a-form-item>
<a-form-item
label="是否启用"
:name="['priority']"
>
<a-switch v-model:checked="selectedFireRule.enabled" />
</a-form-item>
<a-form-item
:wrapper-col="{offset: 6}"
>
<a-space>
<a-button @click="handleSave" type="primary">保存规则</a-button>
<a-popconfirm
v-if="selectedFireRule && selectedFireRule.id > 0"
title="确定删除?"
@confirm="handleDelete"
>
<a-button danger>删除规则</a-button>
</a-popconfirm>
</a-space>
</a-form-item>
</a-form>
</a-col>
</a-row>
</div>
</a-card>
</div>
</Layout>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { type FormInstance, message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import Layout from '../layout.vue';
import { createFireRule, deleteFireRule, findFireRuleByQuery, updateFireRule } from './api';
import type { FireRule, FireRuleRequest } from './types';
import { substring } from '@/utils/strings';
const query = ref<Partial<FireRuleRequest>>({
pageNum: 1,
pageSize: 10,
});
const defaultFireRule: FireRule = {
id: 0,
// 规则名称
name: null,
// 场景类型0-防御1-空降null表示通用
sceneType: null,
// 触发条件JSON格式
conditions: null,
// 响应动作JSON格式
actions: null,
// 优先级(数值越小优先级越高)
priority: 0,
// 是否启用0禁用1启用
enabled: true,
};
const getSceneTypeName = (item: FireRule): string => {
if (0 === item.sceneType) {
return '防御';
}
if (1 === item.sceneType) {
return '空降';
}
return '通用';
};
const resolveItem = (item: FireRule) => {
let newItem = JSON.parse(JSON.stringify(item));
return newItem;
};
const datasource = ref<FireRule[]>([]);
const datasourceTotal = ref<number>(0);
const selectedFireRule = ref<FireRule>(resolveItem(defaultFireRule));
const formRef = ref<FormInstance | null>(null);
const load = () => {
datasource.value = [];
datasourceTotal.value = 0;
formRef.value?.resetFields();
selectedFireRule.value = resolveItem(defaultFireRule);
findFireRuleByQuery(query.value).then(r => {
datasource.value = r.rows ?? [];
datasourceTotal.value = r.total ?? 0;
});
};
const handleCreate = () => {
selectedFireRule.value = resolveItem(defaultFireRule);
};
const handleSelect = (item: FireRule) => {
selectedFireRule.value = resolveItem(item);
formRef.value?.resetFields();
};
const handleDelete = () => {
if (selectedFireRule.value && selectedFireRule.value.id > 0) {
deleteFireRule(selectedFireRule.value.id).then(r => {
if (r.code === 200) {
load();
message.info(r.msg ?? '删除成功');
} else {
message.error(r.msg ?? '删除失败');
}
});
}
};
const handleSave = () => {
if (formRef.value) {
formRef.value.validate().then(() => {
let res = null;
let savedValue: FireRule = JSON.parse(JSON.stringify(selectedFireRule.value)) as any as FireRule;
if (savedValue.id > 0) {
res = updateFireRule(savedValue);
} else {
res = createFireRule(savedValue);
}
if (res) {
res.then(r => {
if (r.code === 200) {
load();
message.info(r.msg ?? '操作成功');
} else {
message.error(r.msg ?? '操作失败');
}
});
}
});
}
};
const handleChange = (page: number, pageSize: number) => {
query.value.pageNum = page;
query.value.pageSize = pageSize;
load();
};
onMounted(() => load());
</script>

View File

@@ -0,0 +1,34 @@
/*
* 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, PageableResponse } from '@/types';
export interface FireRule {
id: number,
// 规则名称
name: NullableString,
// 场景类型0-防御1-空降null表示通用
sceneType: Number | null,
// 触发条件JSON格式
conditions: NullableString,
// 响应动作JSON格式
actions: NullableString,
// 优先级(数值越小优先级越高)
priority: number,
// 是否启用0禁用1启用
enabled: boolean,
}
export interface FireRuleRequest extends FireRule {
pageNum: number,
pageSize: number,
}
export interface FireRulePageableResponse extends PageableResponse<FireRule> {
}