Merge remote-tracking branch 'origin/master'

This commit is contained in:
MHW
2026-04-16 09:59:45 +08:00
6 changed files with 357 additions and 30 deletions

View File

@@ -853,6 +853,7 @@
.ks-model-builder-content,
.ks-model-builder-right {
height: calc(100vh - 90px);
overflow: auto;
.ant-card-body {
padding: 6px;
@@ -987,6 +988,7 @@
.ant-collapse-content-box {
max-height: 37vh;
height: fit-content;
overflow: auto;
}
}
@@ -1555,6 +1557,44 @@
border: 1px solid #0f4a7c;
color: #eee;
}
// 修复 mini 模式分页滚动条问题
&.ant-pagination-mini {
height: 24px;
line-height: 24px;
.ant-pagination-simple-pager {
height: 24px;
line-height: 24px;
input {
height: 22px;
line-height: 20px;
padding: 0 4px;
margin: 0 2px;
box-sizing: border-box;
}
}
.ant-pagination-total-text {
height: 24px;
line-height: 24px;
}
.ant-pagination-prev,
.ant-pagination-next,
.ant-pagination-jump-prev,
.ant-pagination-jump-next {
height: 24px;
line-height: 22px;
.ant-pagination-item-link {
height: 22px;
line-height: 20px;
padding: 0;
}
}
}
}

View File

@@ -5,8 +5,13 @@
<a-layout class="ks-layout-body">
<div class="ks-model-builder-body">
<div class="ks-model-builder-left">
<ScenariosCard
ref="scenariosCardRef"
@select-scenario="handleSelectScenario"
/>
<TressCard
ref="treesCardRef"
:scenarioId="currentScenarioId"
@create-tree="handleCreateTree"
@select-tree="handleSelectTree"
/>
@@ -24,7 +29,7 @@
<span>保存</span>
</a-button>
<a-button v-if="currentScenarioId" class="ks-model-builder-save" size="small" @click="handleGoback">
<a-button v-if="fromScenarioPage" class="ks-model-builder-save" size="small" @click="handleGoback">
<BackwardFilled />
<span>返回</span>
</a-button>
@@ -78,6 +83,7 @@ import { findAllBasicPlatforms, findAllNodeCommands } from '../api';
import type { NodeCommand, Platform } from '../types';
import { createTree, findOneTreeById, findOneTreeByPlatformId, updateTree, findSubPlatforms } from './api';
import TressCard from './trees-card.vue';
import ScenariosCard from './scenarios-card.vue';
import NodesCard from './nodes-card.vue';
const TeleportContainer = defineComponent(getTeleport());
@@ -86,6 +92,7 @@ registerNodeElement();
export default defineComponent({
components: {
ScenariosCard,
TressCard,
NodesCard,
Wrapper,
@@ -111,12 +118,14 @@ export default defineComponent({
const selectedModelNode = ref<Node<NodeProperties> | null>(null);
const selectedNodeTaskElement = ref<GraphTaskElement | null>(null);
const changed = ref<boolean>(false);
const scenariosCardRef = ref<InstanceType<typeof ScenariosCard> | null>(null);
const treesCardRef = ref<InstanceType<typeof TressCard> | null>(null);
const platforms = ref<Platform[]>([]);
const subPlatforms = ref<Platform[]>([]);
const nodeCommands = ref<NodeCommand[]>([])
const currentScenarioId = ref<number | null>(null);
const currentScenarioId = ref<number | undefined>();
const currentPlatformId = ref<number | null>(null);
const fromScenarioPage = ref<boolean>(false);
const {
handleGraphEvent,
@@ -261,8 +270,7 @@ export default defineComponent({
const handleSelectTree = (tree: BehaviorTree) => {
destroyGraph();
currentPlatformId.value = null;
currentScenarioId.value = null;
currentPlatformId.value = null;
console.info('handleSelectTree', tree);
findOneTreeById(tree.id).then(r => {
@@ -326,12 +334,27 @@ export default defineComponent({
});
};
const STORAGE_KEY_SCENARIO = 'designer_from_scenario_id';
const resolveQuery = ()=> {
console.log(currentRoute);
let scenarioId = Number(currentRoute.query.scenario);
if (!isNaN(scenarioId)) {
if (!isNaN(scenarioId) && scenarioId > 0) {
currentScenarioId.value = scenarioId;
fromScenarioPage.value = true;
sessionStorage.setItem(STORAGE_KEY_SCENARIO, String(scenarioId));
} else {
currentScenarioId.value = null;
// 尝试从 sessionStorage 恢复(页面刷新或 SPA 内部跳转后 query 参数丢失的情况)
const stored = sessionStorage.getItem(STORAGE_KEY_SCENARIO);
const storedId = Number(stored);
if (stored && !isNaN(storedId) && storedId > 0) {
currentScenarioId.value = storedId;
fromScenarioPage.value = true;
} else {
currentScenarioId.value = undefined;
fromScenarioPage.value = false;
}
}
let platformId = Number(currentRoute.query.platform);
if (!isNaN(platformId)) {
@@ -341,6 +364,11 @@ export default defineComponent({
}
}
// 处理选择场景
const handleSelectScenario = (scenario: any) => {
currentScenarioId.value = scenario.id;
};
const handleCreateTree = () => {
destroyGraph();
@@ -423,15 +451,19 @@ export default defineComponent({
const init = () => {
console.info('init');
nextTick(() => {
initGraph();
window.addEventListener('resize', handleResize);
console.log('节点挂载完成');
resolveQuery();
if (currentPlatformId.value) {
resolveQuery();
if (currentPlatformId.value) {
findOneTreeByPlatformId(currentPlatformId.value).then(r => {
console.log(r);
if (r.data) {
handleSelectTree(r.data);
} else {
@@ -457,6 +489,7 @@ export default defineComponent({
};
const handleGoback = ()=> {
sessionStorage.removeItem(STORAGE_KEY_SCENARIO);
window.location.href = `/app/decision/communication?scenario=${currentScenarioId.value}`
}
@@ -511,8 +544,10 @@ export default defineComponent({
currentScenarioId,
platforms,
subPlatforms,
scenariosCardRef,
treesCardRef,
handleCreateTree,
handleSelectScenario,
currentTreeEditing,
currentBehaviorTree,
currentGraph,
@@ -534,6 +569,7 @@ export default defineComponent({
handleUpdateElement,
handleSelectTree,
handleGoback,
fromScenarioPage,
};
},
});

View File

@@ -129,21 +129,48 @@
</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-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 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>
<template>
<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-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>
</template>
</a-form>
</template>
@@ -454,6 +481,33 @@ export default defineComponent({
</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;

View File

@@ -0,0 +1,179 @@
<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>
</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="loadScenarios()" />
</a-flex>
</div>
<a-list :data-source="scenarios || []" size="small" style="min-height: 25vh">
<template #renderItem="{ item }">
<a-list-item @click="()=> handleSelect(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">
<a-popconfirm
title="确定复制?"
@confirm="()=> handleCopy(item)"
>
<span class="ks-tree-action ks-tree-action-copy" @click.stop style="margin-right: 10px"><CopyOutlined /></span>
</a-popconfirm>
<a-popconfirm
title="确定删除?"
@confirm="()=> handleDelete(item)"
>
<span class="ks-tree-action ks-tree-action-delete" @click.stop><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="totalScenarios"
style="position: unset; margin: 5px 0;"
simple size="small" @change="handleChange" />
</a-collapse-panel>
</a-collapse>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { CopyOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import type { Scenario, ScenarioRequest } from '../communication/types';
import { findScenarioByQuery } from '../communication/api';
import { substring } from '@/utils/strings';
export default defineComponent({
emits: ['select-scenario'],
components: {
CopyOutlined,
PlusOutlined,
DeleteOutlined,
},
setup(_props, { emit }) {
const scenarios = ref<Scenario[]>([]);
const scenarioQuery = ref<Partial<ScenarioRequest>>({
name: null,
pageNum: 1,
pageSize: 8,
});
const activeKey = ref<number>(1);
const totalScenarios = ref<number>(0);
const loadScenarios = (cb?: () => void) => {
findScenarioByQuery(scenarioQuery.value).then(r => {
scenarios.value = r.rows;
totalScenarios.value = r.total ?? 0;
if(cb) cb();
});
};
const handleChange = (page: number, pageSize: number) => {
scenarioQuery.value.pageNum = page;
scenarioQuery.value.pageSize = pageSize;
loadScenarios();
};
const handleDelete = (item: Scenario) => {
// TODO: 实现删除场景的API调用
console.log('删除场景:', item);
loadScenarios();
};
const handleCopy = (item: Scenario) => {
// TODO: 实现复制场景的API调用
console.log('复制场景:', item);
loadScenarios();
};
const handleSelect = (record: Scenario) => {
emit('select-scenario', record);
};
const refresh = () => loadScenarios();
onMounted(() => {
loadScenarios(() => {
if(scenarios.value.length > 0){
emit('select-scenario', scenarios.value[0]);
}
});
});
return {
refresh,
totalScenarios,
substring,
activeKey,
scenarios,
scenarioQuery,
loadScenarios,
handleSelect,
handleChange,
handleDelete,
handleCopy,
};
},
});
</script>
<style scoped>
.ks-tree-actions {
opacity: 0;
transition: opacity 0.3s;
}
.ks-tree-action {
cursor: pointer;
padding: 2px 5px;
border-radius: 3px;
}
.ks-tree-action:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ks-tree-action-copy {
color: #1890ff;
}
.ks-tree-action-delete {
color: #ff4d4f;
}
.a-list-item {
cursor: pointer;
padding: 8px 12px;
transition: background-color 0.3s;
}
.a-list-item:hover {
background-color: #f5f5f5;
}
.a-list-item:hover .ks-tree-actions {
opacity: 1;
}
.icon-scenario::before {
content: '\e6b8';
font-family: 'iconfont';
margin-right: 8px;
color: #82c4e9;
}
</style>

View File

@@ -25,6 +25,7 @@ export interface BehaviorTree {
export interface BehaviorTreeRequest extends BehaviorTree {
pageNum: number,
pageSize: number,
scenarioId?: number,
}

View File

@@ -50,19 +50,24 @@
v-model:current="behaviorTreeQuery.pageNum"
:page-size="behaviorTreeQuery.pageSize"
:total="totalTress"
style="position: unset; margin: 5px 0;"
simple size="small" @change="handleChange" />
</a-collapse-panel>
</a-collapse>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import type { PropType } from 'vue';
import { defineComponent, ref, watch } from 'vue';
import { CheckOutlined, CopyOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue';
import type { BehaviorTree, BehaviorTreeRequest } from './tree';
import { copyTree, deleteOneTreeById, findTreesByQuery } from './api';
import { substring } from '@/utils/strings';
export default defineComponent({
props: {
scenarioId:Number as PropType<number>,
},
emits: ['select-tree', 'create-tree'],
components: {
CheckOutlined,
@@ -77,11 +82,27 @@ export default defineComponent({
name: null,
pageNum: 1,
pageSize: 8,
scenarioId: _props.scenarioId,
});
const activeKey = ref<number>(1);
const totalTress = ref<number>(0);
const loadTress = () => {
watch(
() => _props.scenarioId,
() => {
if (!_props.scenarioId) {
behaviorTrees.value = [];
totalTress.value = 0;
return;
}
behaviorTreeQuery.value.pageNum = 1;
behaviorTreeQuery.value.scenarioId = _props.scenarioId;
loadTress();
},
{ immediate: true }
);
function loadTress(){
findTreesByQuery(behaviorTreeQuery.value).then(r => {
behaviorTrees.value = r.rows;
totalTress.value = r.total ?? 0;
@@ -131,10 +152,6 @@ export default defineComponent({
const refresh = () => loadTress();
onMounted(() => {
loadTress();
});
return {
refresh,
totalTress,