Files
auto-solution/modeler/src/views/decision/designer/properties.vue
2026-03-27 10:03:16 +08:00

398 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 />-->
<!--&lt;!&ndash; <span>添加</span>&ndash;&gt;-->
<!-- </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=groupIndexvalue=当前分组的参数数组)
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>