Files
auto-solution/modeler/src/views/decision/designer/properties.vue

507 lines
20 KiB
Vue
Raw Normal View History

2026-02-08 15:59:14 +08:00
<template>
<div class="ks-model-builder-right">
2026-02-08 22:31:13 +08:00
<template v-if="currentElement || tree">
2026-02-08 15:59:14 +08:00
<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>
2026-02-08 22:09:28 +08:00
2026-02-08 22:31:13 +08:00
<a-tab-pane v-if="tree" key="1" tab="行为树属性">
2026-02-08 22:09:28 +08:00
<a-form
autocomplete="off"
layout="vertical"
name="basic"
style="padding-bottom:15px;"
>
<a-form-item label="行为树名称">
2026-02-08 22:31:13 +08:00
<a-input v-model:value="tree.name" placeholder="行为树名称" size="small" />
2026-02-08 22:09:28 +08:00
</a-form-item>
2026-02-08 22:28:52 +08:00
<a-form-item label="行为树英文名称">
2026-02-08 22:31:13 +08:00
<a-input v-model:value="tree.englishName" placeholder="行为树英文名称" size="small" />
2026-02-08 22:28:52 +08:00
</a-form-item>
2026-02-08 22:09:28 +08:00
<a-form-item label="行为树说明">
2026-02-08 22:31:13 +08:00
<a-textarea v-model:value="tree.description" placeholder="行为树说明" size="small" />
2026-02-08 22:09:28 +08:00
</a-form-item>
</a-form>
</a-tab-pane>
2026-02-08 22:31:13 +08:00
<a-tab-pane v-if="currentElement" key="2" tab="节点属性">
2026-02-08 15:59:14 +08:00
<a-form
autocomplete="off"
layout="vertical"
name="basic"
style="padding-bottom:15px;"
>
<a-form-item label="节点名称">
2026-02-08 22:31:13 +08:00
<a-input v-model:value="currentElement.name" :placeholder="currentElement.name" size="small" />
2026-02-08 15:59:14 +08:00
</a-form-item>
<a-form-item label="节点介绍">
2026-02-08 22:31:13 +08:00
<a-textarea v-model:value="currentElement.description" :placeholder="currentElement.description" size="small" />
2026-02-08 15:59:14 +08:00
</a-form-item>
2026-03-13 10:40:44 +08:00
<a-form-item label="排序">
<a-input-number style="width:100%;" v-model:value="currentElement.order" size="small" />
</a-form-item>
2026-02-08 15:59:14 +08:00
<a-divider />
<a-form-item label="输入">
2026-02-08 22:31:13 +08:00
<a-textarea v-model:value="currentElement.inputs" size="small" />
2026-02-08 15:59:14 +08:00
</a-form-item>
<a-form-item label="输出">
2026-02-08 22:31:13 +08:00
<a-textarea v-model:value="currentElement.outputs" size="small" />
2026-02-08 15:59:14 +08:00
</a-form-item>
2026-03-13 10:40:44 +08:00
<!-- <a-divider v-if="currentElement.settings && currentElement.parameters.length > 0" />-->
2026-02-08 15:59:14 +08:00
2026-03-13 10:40:44 +08:00
<!-- <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>-->
2026-02-08 15:59:14 +08:00
</a-form>
</a-tab-pane>
</a-tabs>
2026-03-27 09:33:46 +08:00
<a-tabs v-if="currentElement?.parameters && currentElement?.parameters.length > 0" v-model:activeKey="activeBottomTabsKey" class="ks-model-builder-tabs parameters-tabs">
2026-02-08 15:59:14 +08:00
<template #leftExtra>
<span class="ks-model-builder-title-icon icon-input"></span>
</template>
2026-03-26 22:05:31 +08:00
2026-03-26 23:36:47 +08:00
<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="getPlatformTabName(index)" :closable="true">
2026-03-26 23:36:47 +08:00
<a-form-item v-for="setting in grouped" :label="setting.description">
2026-03-27 00:05:04 +08:00
<a-input-number v-if="setting.dataType === 'double'"
v-model:value="setting.defaultValue"
2026-03-26 23:36:47 +08:00
:placeholder="setting.description" size="small" style="width:100%;" />
2026-03-27 00:05:04 +08:00
<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 getAvailablePlatforms()" :value="pl.name">{{pl.description}}</a-select-option>
2026-03-27 00:05:04 +08:00
</a-select>
2026-03-31 14:32:54 +08:00
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'command'" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
</a-select>
2026-03-27 00:05:04 +08:00
<a-input v-else v-model:value="setting.defaultValue"
:placeholder="setting.description" size="small" />
2026-03-26 23:36:47 +08:00
</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>
<template v-for="setting in currentElement.parameters">
<div v-if="['lon','lat'].includes(setting.paramKey as string)">
<div v-if="setting.paramKey==='lon'" class="ks-location-title">位置</div>
<a-form-item class="ks-location-item" labelAlign="left" :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>
</div>
<a-form-item v-else :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 getAvailablePlatforms()" :value="pl.name">{{ pl.description }}</a-select-option>
</a-select>
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'command'" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
</a-select>
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
</a-form-item>
</template>
2026-03-26 23:36:47 +08:00
</template>
</a-form>
</template>
2026-03-13 10:40:44 +08:00
<a-empty v-else>
</a-empty>
2026-02-08 15:59:14 +08:00
</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';
2026-03-26 23:36:47 +08:00
import { CheckOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
import type { ElementParameter, ElementVariable, GraphTaskElement } from '../graph';
2026-03-15 16:36:07 +08:00
import type { BehaviorTree } from './tree';
2026-02-08 15:59:14 +08:00
import type { Graph, Node, NodeProperties } from '@antv/x6';
2026-02-08 20:27:40 +08:00
import { generateKey } from '@/utils/strings';
2026-03-31 14:32:54 +08:00
import type { NodeCommand, Platform } from '../types';
2026-02-08 15:59:14 +08:00
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({
2026-03-26 23:36:47 +08:00
components: { CheckOutlined, PlusOutlined, PlusCircleOutlined },
2026-02-08 15:59:14 +08:00
props: {
2026-02-08 22:09:28 +08:00
tree: { type: [Object, null] as PropType<BehaviorTree | null | undefined>, required: false },
2026-02-08 22:31:13 +08:00
treeEditing: { type: Boolean as PropType<boolean>, required: true, default: false },
2026-02-08 15:59:14 +08:00
node: { type: [Object, null] as PropType<Node<NodeProperties> | null | undefined>, required: false },
2026-02-08 22:31:13 +08:00
graph: { type: [Object, null] as PropType<Graph | null | undefined>, required: true },
2026-03-27 00:05:04 +08:00
platforms: { type: Array as PropType<Platform[]>, required: true },
subPlatforms: { type: Array as PropType<Platform[]>, required: false, default: () => [] },
2026-03-31 14:32:54 +08:00
nodeCommands: { type: Array as PropType<NodeCommand[]>, required: true },
2026-02-08 15:59:14 +08:00
},
2026-02-08 22:09:28 +08:00
emits: ['update-element', 'update-tree'],
2026-02-08 15:59:14 +08:00
setup(props, { emit }) {
2026-03-27 00:05:04 +08:00
const platforms = ref<Platform[]>(props.platforms ?? []);
const subPlatforms = ref<Platform[]>(props.subPlatforms ?? []);
2026-03-31 14:32:54 +08:00
const nodeCommands = ref<NodeCommand[]>(props.nodeCommands ?? []);
2026-03-27 00:05:04 +08:00
2026-02-08 15:59:14 +08:00
const activeTopTabsKey = ref<string>('1');
const activeBottomTabsKey = ref<string>('1');
const activeBottomTabs2Key = ref<string>('1');
2026-02-08 22:31:13 +08:00
const currentTree = ref<BehaviorTree | null>(props.tree ?? null);
2026-02-08 22:09:28 +08:00
const treeEditing = ref<boolean>(props.treeEditing);
2026-02-08 15:59:14 +08:00
const currentNode = ref<Node | null>(props.node ?? null);
2026-02-08 20:27:40 +08:00
const currentElement = ref<GraphTaskElement | null>(null);
2026-02-08 15:59:14 +08:00
2026-03-26 23:36:47 +08:00
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();
// 如果有下属平台,预填对应索引的平台名称
const nextIndex = groupedParameters.value.length;
const subPlatform = subPlatforms.value[nextIndex];
if (subPlatform) {
const platformParam = newParameters.find(p => p.paramKey === 'platforms');
if (platformParam) {
platformParam.defaultValue = subPlatform.name;
}
}
// 新增一个参数分组
2026-03-26 23:36:47 +08:00
groupedParameters.value.push(newParameters);
// 自动切换到新增的分组
groupedParametersActiveTab.value = groupedParameters.value.length - 1;
2026-02-08 15:59:14 +08:00
};
2026-03-26 23:36:47 +08:00
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;
};
// 获取平台Tab显示名称优先使用下属平台名称
const getPlatformTabName = (index: number): string => {
const sub = subPlatforms.value[index];
if (sub) {
return sub.name || sub.description || `平台 ${index + 1}`;
}
return `平台 ${index + 1}`;
};
// 获取可用的平台列表(包括当前平台和其下属平台)
const getAvailablePlatforms = (): Platform[] => {
if (!currentTree.value?.platformId) {
return platforms.value;
}
// 如果有下属平台,返回下属平台列表
if (subPlatforms.value && subPlatforms.value.length > 0) {
return subPlatforms.value;
}
// 否则返回所有平台
return platforms.value;
};
2026-03-26 23:36:47 +08:00
2026-02-08 15:59:14 +08:00
const resolveNode = (n?: Node | null | undefined) => {
2026-03-26 23:36:47 +08:00
groupedParametersActiveTab.value = 0;
2026-02-08 15:59:14 +08:00
currentNode.value = n ?? null;
if (n) {
const data = n.getData();
2026-02-08 20:27:40 +08:00
currentElement.value = JSON.parse(JSON.stringify(data || {})) as GraphTaskElement;
2026-02-08 15:59:14 +08:00
} else {
currentElement.value = null;
}
2026-03-26 23:36:47 +08:00
resolveGroupedParameters();
2026-02-08 15:59:14 +08:00
};
2026-02-08 22:31:13 +08:00
const addVariable = () => {
2026-02-08 15:59:14 +08:00
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,
2026-02-08 22:31:13 +08:00
});
};
2026-02-08 15:59:14 +08:00
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) {
// 深拷贝当前元素数据
2026-02-08 20:27:40 +08:00
const newElement = JSON.parse(JSON.stringify(currentElement.value)) as GraphTaskElement;
2026-03-26 23:36:47 +08:00
if (multiableParameters.value) {
newElement.parameters = groupedParameters.value.flatMap((group, groupIndex) => {
// 遍历每个分组,给组内所有参数统一设置/保持 groupIndex
return group.map(param => ({
...param,
groupIndex: groupIndex // 强制保证:当前参数的分组索引 = 所在分组的索引
}));
});
}
2026-02-08 15:59:14 +08:00
// 更新节点数据
currentNode.value.replaceData(newElement);
// 触发事件通知父组件
emit('update-element', newElement);
}
};
2026-03-26 23:36:47 +08:00
const load = () => {
};
2026-02-08 15:59:14 +08:00
watch(
() => props.node,
(n?: Node | null | undefined) => resolveNode(n),
{ deep: true, immediate: true },
);
2026-02-08 22:09:28 +08:00
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 },
);
2026-03-26 23:36:47 +08:00
watch(() => groupedParameters.value, () => updateNode(), { deep: true });
2026-02-08 15:59:14 +08:00
watch(() => currentElement.value, () => updateNode(), { deep: true });
2026-03-31 14:32:54 +08:00
watch(() => props.nodeCommands, (n: NodeCommand[] | null | undefined) => nodeCommands.value = n ?? [], { deep: true, immediate: true });
2026-03-27 00:05:04 +08:00
watch(() => props.platforms, (n: Platform[] | null | undefined) => platforms.value = n ?? [], { deep: true, immediate: true });
watch(() => props.subPlatforms, (n: Platform[] | null | undefined) => subPlatforms.value = n ?? [], { deep: true, immediate: true });
2026-03-27 00:05:04 +08:00
2026-02-08 15:59:14 +08:00
onMounted(() => load());
return {
2026-03-31 14:32:54 +08:00
nodeCommands,
2026-03-27 00:05:04 +08:00
platforms,
2026-03-26 23:36:47 +08:00
addParameterTab,
groupedParametersActiveTab,
multiableParameters,
onEditParameterTab,
groupedParameters,
getPlatformTabName,
getAvailablePlatforms,
2026-02-08 15:59:14 +08:00
actionSpaceColumns,
activeTopTabsKey,
activeBottomTabsKey,
activeBottomTabs2Key,
currentElement,
addVariable,
removeVariable,
2026-02-08 22:09:28 +08:00
currentTree,
treeEditing,
2026-02-08 15:59:14 +08:00
// currentElementParameters,
};
},
});
</script>
<style scoped lang="less">
.ks-location-title {
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.65);
margin-top: 6px;
margin-bottom: 6px;
}
:deep(.ks-location-item) {
.ant-form-item-row {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
}
.ant-form-item-label {
width: 48px;
flex-shrink: 0;
padding-bottom: 0;
> label {
height: 24px;
}
}
.ant-form-item-control {
flex: 1;
}
}
.ks-parameter-setting-tabs {
:deep(.ant-tabs-tab) {
width: 120px;
max-width: 120px;
}
:deep(.ant-tabs-tab-btn) {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>