398 lines
15 KiB
Vue
398 lines
15 KiB
Vue
<template>
|
||
<div class="ks-model-builder-right">
|
||
<template v-if="currentElement || tree">
|
||
|
||
<a-tabs v-model:activeKey="activeTopTabsKey" class="ks-model-builder-tabs settings-tab">
|
||
<template #leftExtra>
|
||
<span class="ks-model-builder-title-icon icon-input"></span>
|
||
</template>
|
||
|
||
<a-tab-pane v-if="tree" key="1" tab="行为树属性">
|
||
<a-form
|
||
autocomplete="off"
|
||
layout="vertical"
|
||
name="basic"
|
||
style="padding-bottom:15px;"
|
||
>
|
||
<a-form-item label="行为树名称">
|
||
<a-input v-model:value="tree.name" placeholder="行为树名称" size="small" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="行为树英文名称">
|
||
<a-input v-model:value="tree.englishName" placeholder="行为树英文名称" size="small" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="行为树说明">
|
||
<a-textarea v-model:value="tree.description" placeholder="行为树说明" size="small" />
|
||
</a-form-item>
|
||
|
||
</a-form>
|
||
</a-tab-pane>
|
||
|
||
<a-tab-pane v-if="currentElement" key="2" tab="节点属性">
|
||
<a-form
|
||
autocomplete="off"
|
||
layout="vertical"
|
||
name="basic"
|
||
style="padding-bottom:15px;"
|
||
>
|
||
<a-form-item label="节点名称">
|
||
<a-input v-model:value="currentElement.name" :placeholder="currentElement.name" size="small" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="节点介绍">
|
||
<a-textarea v-model:value="currentElement.description" :placeholder="currentElement.description" size="small" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="排序">
|
||
<a-input-number style="width:100%;" v-model:value="currentElement.order" size="small" />
|
||
</a-form-item>
|
||
|
||
<a-divider />
|
||
|
||
<a-form-item label="输入">
|
||
<a-textarea v-model:value="currentElement.inputs" size="small" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="输出">
|
||
<a-textarea v-model:value="currentElement.outputs" size="small" />
|
||
</a-form-item>
|
||
|
||
<!-- <a-divider v-if="currentElement.settings && currentElement.parameters.length > 0" />-->
|
||
|
||
<!-- <a-form-item v-for="setting in currentElement.parameters" :label="setting.description">-->
|
||
<!-- <a-input-number v-if="setting.dataType === 'double'" v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" style="width:100%;" />-->
|
||
<!-- <a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />-->
|
||
<!-- </a-form-item>-->
|
||
</a-form>
|
||
</a-tab-pane>
|
||
|
||
</a-tabs>
|
||
|
||
<a-tabs v-if="currentElement?.parameters && currentElement?.parameters.length > 0" v-model:activeKey="activeBottomTabsKey" class="ks-model-builder-tabs parameters-tabs">
|
||
<template #leftExtra>
|
||
<span class="ks-model-builder-title-icon icon-input"></span>
|
||
</template>
|
||
|
||
<template #rightExtra v-if="multiableParameters">
|
||
<a-tooltip title="添加平台" placement="left">
|
||
<div class="ks-add-parameter-action" @click="()=> addParameterTab()">
|
||
<a-space>
|
||
<PlusCircleOutlined/>
|
||
<span>添加</span>
|
||
</a-space>
|
||
</div>
|
||
</a-tooltip>
|
||
</template>
|
||
<a-tab-pane key="1" tab="节点变量">
|
||
<template v-if="currentElement.parameters && currentElement.parameters.length > 0">
|
||
<a-form
|
||
autocomplete="off"
|
||
layout="vertical"
|
||
name="basic"
|
||
style="padding-bottom:15px;"
|
||
>
|
||
|
||
<template v-if="multiableParameters">
|
||
<a-tabs class="ks-parameter-setting-tabs"
|
||
v-model:activeKey="groupedParametersActiveTab"
|
||
tab-position="left"
|
||
hide-add
|
||
size="small"
|
||
type="editable-card"
|
||
@edit="onEditParameterTab">
|
||
<a-tab-pane v-for="(grouped,index) in groupedParameters" :key="index" :tab="`平台 ${index + 1}`" :closable="true">
|
||
<a-form-item v-for="setting in grouped" :label="setting.description">
|
||
<a-input-number v-if="setting.dataType === 'double'"
|
||
v-model:value="setting.defaultValue"
|
||
:placeholder="setting.description" size="small" style="width:100%;" />
|
||
<a-select :placeholder="`请选择${setting.description}`"
|
||
allow-clear
|
||
v-else-if="setting.paramKey === 'platforms'" v-model:value="setting.defaultValue">
|
||
<a-select-option v-for="pl in platforms" :value="pl.name">{{pl.description}}</a-select-option>
|
||
</a-select>
|
||
<a-input v-else v-model:value="setting.defaultValue"
|
||
:placeholder="setting.description" size="small" />
|
||
</a-form-item>
|
||
</a-tab-pane>
|
||
<!-- <template #leftExtra>-->
|
||
<!-- <a-button class="ks-btn" size="small" style="margin-bottom: 15px; width: 100%" block @click="()=> addParameterTab()">-->
|
||
<!-- <PlusOutlined />-->
|
||
<!--<!– <span>添加</span>–>-->
|
||
<!-- </a-button>-->
|
||
<!-- </template>-->
|
||
</a-tabs>
|
||
</template>
|
||
<template v-else>
|
||
<a-form-item v-for="setting in currentElement.parameters" :label="setting.description">
|
||
<a-input-number v-if="setting.dataType === 'double'" v-model:value="setting.defaultValue"
|
||
:placeholder="setting.description" size="small" style="width:100%;" />
|
||
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
|
||
</a-form-item>
|
||
</template>
|
||
</a-form>
|
||
</template>
|
||
<a-empty v-else>
|
||
</a-empty>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
|
||
</template>
|
||
|
||
<a-tabs v-else :activeKey="'0'" class="ks-model-builder-tabs parameters-tabs empty">
|
||
<template #leftExtra>
|
||
<span class="ks-model-builder-title-icon icon-input"></span>
|
||
</template>
|
||
<a-tab-pane :key="'0'" tab="请选择或者创建决策树">
|
||
<a-empty>
|
||
<template #description>
|
||
请选择或者创建决策树
|
||
</template>
|
||
</a-empty>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { defineComponent, onMounted, type PropType, ref, watch } from 'vue';
|
||
import { CheckOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||
import type { ElementParameter, ElementVariable, GraphTaskElement } from '../graph';
|
||
import type { BehaviorTree } from './tree';
|
||
import type { Graph, Node, NodeProperties } from '@antv/x6';
|
||
import { generateKey } from '@/utils/strings';
|
||
import type { Platform } from '@/views/decision/types';
|
||
|
||
const actionSpaceColumns = [
|
||
{ title: '序号', dataIndex: 'index', key: 'index', width: 40 },
|
||
{ title: '变量名', dataIndex: 'name', key: 'name', width: 80 },
|
||
{ title: '参数值', dataIndex: 'value', key: 'value', width: 80 },
|
||
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 80 },
|
||
{ title: '操作', dataIndex: '_actions', key: '_actions', width: 60 },
|
||
];
|
||
|
||
export default defineComponent({
|
||
components: { CheckOutlined, PlusOutlined, PlusCircleOutlined },
|
||
props: {
|
||
tree: { type: [Object, null] as PropType<BehaviorTree | null | undefined>, required: false },
|
||
treeEditing: { type: Boolean as PropType<boolean>, required: true, default: false },
|
||
node: { type: [Object, null] as PropType<Node<NodeProperties> | null | undefined>, required: false },
|
||
graph: { type: [Object, null] as PropType<Graph | null | undefined>, required: true },
|
||
platforms: { type: Array as PropType<Platform[]>, required: true },
|
||
},
|
||
emits: ['update-element', 'update-tree'],
|
||
setup(props, { emit }) {
|
||
const platforms = ref<Platform[]>(props.platforms ?? []);
|
||
|
||
const activeTopTabsKey = ref<string>('1');
|
||
const activeBottomTabsKey = ref<string>('1');
|
||
const activeBottomTabs2Key = ref<string>('1');
|
||
|
||
const currentTree = ref<BehaviorTree | null>(props.tree ?? null);
|
||
const treeEditing = ref<boolean>(props.treeEditing);
|
||
const currentNode = ref<Node | null>(props.node ?? null);
|
||
const currentElement = ref<GraphTaskElement | null>(null);
|
||
|
||
const emptyParameters = ref<ElementParameter[]>([]);
|
||
const groupedParameters = ref<Array<ElementParameter[]>>([]);
|
||
const multiableParameters = ref<boolean>(false);
|
||
const groupedParametersActiveTab = ref<number>(0);
|
||
|
||
const createEmptyParameters = (): ElementParameter[] => {
|
||
try {
|
||
return JSON.parse(JSON.stringify(currentElement.value?.parameters ?? [])) as ElementParameter[];
|
||
} catch (e: any) {
|
||
return [];
|
||
}
|
||
};
|
||
|
||
const dumpParameters = (): ElementParameter[] => {
|
||
return JSON.parse(JSON.stringify(emptyParameters.value)) as ElementParameter[];
|
||
};
|
||
|
||
const addParameterTab = () => {
|
||
let newParameters = dumpParameters();
|
||
// 新增一个空的参数分组
|
||
groupedParameters.value.push(newParameters);
|
||
// 自动切换到新增的分组
|
||
groupedParametersActiveTab.value = groupedParameters.value.length - 1;
|
||
};
|
||
|
||
|
||
const removeParameterTab = (index: number) => {
|
||
// 边界判断:防止删除不存在的分组 / 只剩一个分组时禁止删除
|
||
if (index < 0 || index >= groupedParameters.value.length) return;
|
||
if (groupedParameters.value.length <= 1) return;
|
||
|
||
// 从数组中删除对应索引的分组
|
||
groupedParameters.value.splice(index, 1);
|
||
|
||
if (groupedParameters.value.length === 0) {
|
||
groupedParameters.value.push(dumpParameters());
|
||
}
|
||
|
||
// 删除后处理激活状态:
|
||
// 如果删除的是最后一个分组,激活前一个
|
||
if (groupedParametersActiveTab.value >= groupedParameters.value.length) {
|
||
groupedParametersActiveTab.value = groupedParameters.value.length - 1;
|
||
}
|
||
|
||
};
|
||
|
||
const onEditParameterTab = (targetKey: number | MouseEvent, action: string) => {
|
||
if (action === 'add') {
|
||
addParameterTab();
|
||
} else {
|
||
removeParameterTab(targetKey as number);
|
||
}
|
||
};
|
||
|
||
const resolveGroupedParameters = () => {
|
||
emptyParameters.value = createEmptyParameters();
|
||
// 解构获取当前元素的关键属性,简化代码
|
||
const { multiable, parameters } = currentElement.value || {};
|
||
|
||
// 1. 不满足多分组条件:直接清空分组
|
||
if (multiable !== true || !parameters || parameters.length === 0) {
|
||
groupedParameters.value = [];
|
||
multiableParameters.value = multiable === true;
|
||
return;
|
||
}
|
||
|
||
// 2. 满足条件:根据 groupIndex 对参数进行分组
|
||
// 第一步:用 Map 做临时分组(key=groupIndex,value=当前分组的参数数组)
|
||
const groupMap = new Map<number, ElementParameter[]>();
|
||
parameters.forEach(param => {
|
||
const index = param.groupIndex;
|
||
// 如果 Map 中没有该分组,先初始化空数组
|
||
if (!groupMap.has(index)) {
|
||
groupMap.set(index, []);
|
||
}
|
||
// 将当前参数推入对应分组
|
||
groupMap.get(index)!.push(param);
|
||
});
|
||
|
||
// 第二步:将 Map 转换为二维数组(按 groupIndex 升序排序)
|
||
groupedParameters.value = Array.from(groupMap.entries())
|
||
// 按分组索引从小到大排序(保证分组顺序正确)
|
||
.sort((a, b) => a[0] - b[0])
|
||
// 只保留分组后的参数数组,丢弃 key
|
||
.map(item => item[1]);
|
||
|
||
multiableParameters.value = multiable === true && groupedParameters.value.length > 0;
|
||
};
|
||
|
||
|
||
const resolveNode = (n?: Node | null | undefined) => {
|
||
groupedParametersActiveTab.value = 0;
|
||
currentNode.value = n ?? null;
|
||
if (n) {
|
||
const data = n.getData();
|
||
currentElement.value = JSON.parse(JSON.stringify(data || {})) as GraphTaskElement;
|
||
} else {
|
||
currentElement.value = null;
|
||
}
|
||
resolveGroupedParameters();
|
||
};
|
||
|
||
const addVariable = () => {
|
||
if (!currentElement.value) {
|
||
return;
|
||
}
|
||
|
||
if (!currentElement.value.variables) {
|
||
currentElement.value.variables = [];
|
||
}
|
||
currentElement.value?.variables.push({
|
||
key: generateKey('variable'),
|
||
name: null,
|
||
value: null,
|
||
defaults: null,
|
||
unit: null,
|
||
});
|
||
};
|
||
|
||
const removeVariable = (row: ElementVariable) => {
|
||
if (currentElement.value && currentElement.value.variables) {
|
||
const filteredVars = currentElement.value.variables.filter(v => v.key !== row.key);
|
||
currentElement.value.variables = [...filteredVars];
|
||
}
|
||
};
|
||
|
||
const updateNode = () => {
|
||
if (currentNode.value && currentElement.value) {
|
||
// 深拷贝当前元素数据
|
||
const newElement = JSON.parse(JSON.stringify(currentElement.value)) as GraphTaskElement;
|
||
|
||
if (multiableParameters.value) {
|
||
newElement.parameters = groupedParameters.value.flatMap((group, groupIndex) => {
|
||
// 遍历每个分组,给组内所有参数统一设置/保持 groupIndex
|
||
return group.map(param => ({
|
||
...param,
|
||
groupIndex: groupIndex // 强制保证:当前参数的分组索引 = 所在分组的索引
|
||
}));
|
||
});
|
||
}
|
||
|
||
// 更新节点数据
|
||
currentNode.value.replaceData(newElement);
|
||
// 触发事件通知父组件
|
||
emit('update-element', newElement);
|
||
}
|
||
};
|
||
|
||
const load = () => {
|
||
};
|
||
|
||
watch(
|
||
() => props.node,
|
||
(n?: Node | null | undefined) => resolveNode(n),
|
||
{ deep: true, immediate: true },
|
||
);
|
||
|
||
watch(
|
||
() => props.tree,
|
||
(n?: BehaviorTree | null | undefined) => {
|
||
currentTree.value = n ?? null;
|
||
},
|
||
{ deep: true, immediate: true },
|
||
);
|
||
|
||
watch(
|
||
() => props.treeEditing,
|
||
(n?: boolean | null | undefined) => {
|
||
treeEditing.value = n === true;
|
||
},
|
||
{ deep: true, immediate: true },
|
||
);
|
||
|
||
watch(() => groupedParameters.value, () => updateNode(), { deep: true });
|
||
|
||
watch(() => currentElement.value, () => updateNode(), { deep: true });
|
||
|
||
watch(() => props.platforms, (n: Platform[] | null | undefined) => platforms.value = n ?? [], { deep: true, immediate: true });
|
||
|
||
onMounted(() => load());
|
||
|
||
return {
|
||
platforms,
|
||
addParameterTab,
|
||
groupedParametersActiveTab,
|
||
multiableParameters,
|
||
onEditParameterTab,
|
||
groupedParameters,
|
||
actionSpaceColumns,
|
||
activeTopTabsKey,
|
||
activeBottomTabsKey,
|
||
activeBottomTabs2Key,
|
||
currentElement,
|
||
addVariable,
|
||
removeVariable,
|
||
currentTree,
|
||
treeEditing,
|
||
// currentElementParameters,
|
||
};
|
||
},
|
||
});
|
||
</script>
|