feat: 添加机器人标签功能,更新相关接口和组件以支持机器人标签的管理和显示
This commit is contained in:
parent
5606f15f11
commit
a87909a736
@ -10,6 +10,13 @@ export interface RobotGroup {
|
|||||||
robots?: Array<string>; // 机器人列表
|
robots?: Array<string>; // 机器人列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RobotLabel {
|
||||||
|
sid?: string; // 场景id
|
||||||
|
id: string; // 机器人标签id
|
||||||
|
label: string; // 机器人标签名称
|
||||||
|
robots?: Array<string>; // 机器人列表
|
||||||
|
}
|
||||||
|
|
||||||
export type RobotPen = Pen & {
|
export type RobotPen = Pen & {
|
||||||
robot: {
|
robot: {
|
||||||
type: RobotType;
|
type: RobotType;
|
||||||
@ -25,6 +32,7 @@ export type RobotPen = Pen & {
|
|||||||
};
|
};
|
||||||
export interface RobotInfo {
|
export interface RobotInfo {
|
||||||
gid?: string; // 机器人组id
|
gid?: string; // 机器人组id
|
||||||
|
lid?: string[]; // 机器人标签id
|
||||||
id: string; // 机器人id
|
id: string; // 机器人id
|
||||||
label: string; // 机器人名称
|
label: string; // 机器人名称
|
||||||
brand: RobotBrand; // 机器人品牌
|
brand: RobotBrand; // 机器人品牌
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { RobotGroup, RobotInfo } from '@api/robot';
|
import type { RobotGroup, RobotInfo, RobotLabel } from '@api/robot';
|
||||||
import type { Meta2dData } from '@meta2d/core';
|
import type { Meta2dData } from '@meta2d/core';
|
||||||
|
|
||||||
import type { EditorColorConfig } from '../../services/color/color-config.service';
|
import type { EditorColorConfig } from '../../services/color/color-config.service';
|
||||||
@ -17,6 +17,7 @@ export interface GroupSceneDetail extends SceneDetail {
|
|||||||
|
|
||||||
export interface SceneData extends Meta2dData {
|
export interface SceneData extends Meta2dData {
|
||||||
robotGroups?: Array<RobotGroup>; // 机器人组信息
|
robotGroups?: Array<RobotGroup>; // 机器人组信息
|
||||||
|
robotLabels?: Array<RobotLabel>; // 机器人标签信息
|
||||||
robots?: Array<RobotInfo>; // 机器人信息
|
robots?: Array<RobotInfo>; // 机器人信息
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ export interface StandardScene {
|
|||||||
scale?: number; // 缩放比例
|
scale?: number; // 缩放比例
|
||||||
origin?: { x: number; y: number }; // 默认载入原点
|
origin?: { x: number; y: number }; // 默认载入原点
|
||||||
robotGroups?: Array<RobotGroup>; // 机器人组信息
|
robotGroups?: Array<RobotGroup>; // 机器人组信息
|
||||||
|
robotLabels?: Array<RobotLabel>; // 机器人标签信息
|
||||||
robots?: Array<RobotInfo>; // 机器人信息
|
robots?: Array<RobotInfo>; // 机器人信息
|
||||||
points?: Array<StandardScenePoint>; // 标准点位信息
|
points?: Array<StandardScenePoint>; // 标准点位信息
|
||||||
routes?: Array<StandardSceneRoute>; // 标准线路信息
|
routes?: Array<StandardSceneRoute>; // 标准线路信息
|
||||||
|
|||||||
@ -14,12 +14,23 @@ const editor = inject(props.token)!;
|
|||||||
|
|
||||||
export type RobotAddModalRef = Ref;
|
export type RobotAddModalRef = Ref;
|
||||||
type Ref = {
|
type Ref = {
|
||||||
open: (gid: string) => void;
|
open: (id: string, type: 'group' | 'label') => void;
|
||||||
};
|
};
|
||||||
const open: Ref['open'] = async (id) => {
|
const open: Ref['open'] = async (id, t) => {
|
||||||
const res = await getAllRobots();
|
const allBackendRobots = await getAllRobots();
|
||||||
robots.value = res.filter(({ id }) => isNil(editor.value.getRobotById(id)));
|
targetId.value = id;
|
||||||
gid.value = id;
|
targetType.value = t;
|
||||||
|
|
||||||
|
if (t === 'group') {
|
||||||
|
// 机器人组的逻辑:只显示尚未添加到场景中的机器人
|
||||||
|
robots.value = allBackendRobots.filter(({ id }) => isNil(editor.value.getRobotById(id)));
|
||||||
|
} else {
|
||||||
|
// 机器人标签的逻辑:显示所有机器人,但排除已在该标签中的
|
||||||
|
const label = editor.value.robotLabels.value?.find((l) => l.id === id);
|
||||||
|
const robotsInLabelIds = new Set(label?.robots ?? []);
|
||||||
|
robots.value = allBackendRobots.filter((robot) => !robotsInLabelIds.has(robot.id));
|
||||||
|
}
|
||||||
|
|
||||||
keyword.value = '';
|
keyword.value = '';
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
show.value = true;
|
show.value = true;
|
||||||
@ -28,7 +39,8 @@ defineExpose<Ref>({ open });
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const gid = ref<RobotInfo['gid']>();
|
const targetId = ref<string>();
|
||||||
|
const targetType = ref<'group' | 'label'>();
|
||||||
|
|
||||||
const show = ref<boolean>(false);
|
const show = ref<boolean>(false);
|
||||||
const keyword = ref<string>('');
|
const keyword = ref<string>('');
|
||||||
@ -41,7 +53,11 @@ const selected = ref<RobotInfo['id'][]>([]);
|
|||||||
const submit = () => {
|
const submit = () => {
|
||||||
try {
|
try {
|
||||||
const temp = selected.value.map((v) => robots.value.find(({ id }) => id === v)).filter((v) => !isNil(v));
|
const temp = selected.value.map((v) => robots.value.find(({ id }) => id === v)).filter((v) => !isNil(v));
|
||||||
editor.value.addRobots(gid.value, temp);
|
if (targetType.value === 'group') {
|
||||||
|
editor.value.addRobots(targetId.value, temp);
|
||||||
|
} else {
|
||||||
|
editor.value.addRobotsToLabel(targetId.value!, temp);
|
||||||
|
}
|
||||||
show.value = false;
|
show.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isError(error)) {
|
if (isError(error)) {
|
||||||
|
|||||||
56
src/components/modal/robot-label-rename-modal.vue
Normal file
56
src/components/modal/robot-label-rename-modal.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { EditorService } from '@core/editor.service';
|
||||||
|
import { type FormInstance, message } from 'ant-design-vue';
|
||||||
|
import { isError } from 'lodash-es';
|
||||||
|
import { inject, type InjectionKey, reactive, ref, type ShallowRef, shallowRef } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
token: InjectionKey<ShallowRef<EditorService>>;
|
||||||
|
};
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const editor = inject(props.token)!;
|
||||||
|
|
||||||
|
export type RobotLabelRenameModalRef = Ref;
|
||||||
|
type Ref = {
|
||||||
|
open: (id: string, label: string) => void;
|
||||||
|
};
|
||||||
|
const open: Ref['open'] = (id, label) => {
|
||||||
|
form.value?.resetFields();
|
||||||
|
data.id = id;
|
||||||
|
data.label = label;
|
||||||
|
show.value = true;
|
||||||
|
};
|
||||||
|
defineExpose<Ref>({ open });
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const show = ref<boolean>(false);
|
||||||
|
|
||||||
|
const form = shallowRef<FormInstance>();
|
||||||
|
const data = reactive<{ id: string; label: string }>({ id: '', label: '' });
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
await form.value?.validate();
|
||||||
|
editor.value.updateRobotLabel(data.id, data.label);
|
||||||
|
show.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
if (isError(error)) {
|
||||||
|
message.error(t(error?.message));
|
||||||
|
} else {
|
||||||
|
console.debug(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-modal :width="460" :title="$t('修改标签名称')" v-model:open="show" :mask-closable="false" centered @ok="submit">
|
||||||
|
<a-form ref="form" class="mr-14" :model="data">
|
||||||
|
<a-form-item :label="$t('机器人标签名称')" name="label" :rules="[{ required: true, message: '' }]">
|
||||||
|
<a-input :placeholder="$t('请输入')" :maxlength="16" v-model:value="data.label" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
@ -45,7 +45,6 @@ const handleContextMenuClose = () => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
const keyword = ref<string>('');
|
const keyword = ref<string>('');
|
||||||
|
|
||||||
//#region 机器人列表
|
//#region 机器人列表
|
||||||
@ -93,12 +92,12 @@ const handleRobotRightClick = (event: MouseEvent, robot: RobotInfo) => {
|
|||||||
event.target.setAttribute('data-robot-id', robot.id);
|
event.target.setAttribute('data-robot-id', robot.id);
|
||||||
event.target.setAttribute('data-robot-type', 'robot');
|
event.target.setAttribute('data-robot-type', 'robot');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理右键菜单 - handleContextMenu 内部会处理 preventDefault 和 stopPropagation
|
// 处理右键菜单 - handleContextMenu 内部会处理 preventDefault 和 stopPropagation
|
||||||
handleContextMenu(event, contextMenuManager, {
|
handleContextMenu(event, contextMenuManager, {
|
||||||
robotService: {
|
robotService: {
|
||||||
getRobotById: (id: string) => editor.value.getRobotById(id)
|
getRobotById: (id: string) => editor.value.getRobotById(id),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -153,7 +152,7 @@ const toRemoveRobots = () =>
|
|||||||
<RobotAddModal ref="refAddRobot" :token="token" />
|
<RobotAddModal ref="refAddRobot" :token="token" />
|
||||||
<RobotRegisterModal ref="refRegisterRobot" :token="token" />
|
<RobotRegisterModal ref="refRegisterRobot" :token="token" />
|
||||||
<RobotGroupRenameModal ref="refRenameGroup" :token="token" />
|
<RobotGroupRenameModal ref="refRenameGroup" :token="token" />
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
:visible="contextMenuState.visible"
|
:visible="contextMenuState.visible"
|
||||||
@ -201,7 +200,7 @@ const toRemoveRobots = () =>
|
|||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu>
|
<a-menu>
|
||||||
<a-menu-item>
|
<a-menu-item>
|
||||||
<a-space align="center" :size="4" @click="refAddRobot?.open(id)">
|
<a-space align="center" :size="4" @click="refAddRobot?.open(id, 'group')">
|
||||||
<i class="icon plus size-20" />
|
<i class="icon plus size-20" />
|
||||||
<span>{{ $t('添加机器人') }}</span>
|
<span>{{ $t('添加机器人') }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
@ -306,7 +305,7 @@ const toRemoveRobots = () =>
|
|||||||
.ant-list-item {
|
.ant-list-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|||||||
303
src/components/robot-labels.vue
Normal file
303
src/components/robot-labels.vue
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type RobotInfo, type RobotLabel } from '@api/robot';
|
||||||
|
import type { RobotAddModalRef } from '@common/modal/robot-add-modal.vue';
|
||||||
|
import type { RobotRegisterModalRef } from '@common/modal/robot-register-modal.vue';
|
||||||
|
import type { EditorService } from '@core/editor.service';
|
||||||
|
import { Modal } from 'ant-design-vue';
|
||||||
|
import { computed, inject, type InjectionKey, reactive, ref, type ShallowRef, shallowRef, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { createContextMenuManager, handleContextMenu } from '../services/context-menu';
|
||||||
|
import ContextMenu from './context-menu/context-menu.vue';
|
||||||
|
import type { RobotLabelRenameModalRef } from './modal/robot-label-rename-modal.vue';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
token: InjectionKey<ShallowRef<EditorService>>;
|
||||||
|
sid: string;
|
||||||
|
showLabelEdit?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
current?: string;
|
||||||
|
};
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const editor = inject(props.token)!;
|
||||||
|
|
||||||
|
type Events = {
|
||||||
|
(e: 'change', id: string): void;
|
||||||
|
};
|
||||||
|
const emit = defineEmits<Events>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
//#region 右键菜单
|
||||||
|
const contextMenuManager = createContextMenuManager();
|
||||||
|
const contextMenuState = ref(contextMenuManager.getState());
|
||||||
|
|
||||||
|
// 订阅右键菜单状态变化
|
||||||
|
contextMenuManager.subscribe((state) => {
|
||||||
|
contextMenuState.value = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理右键菜单关闭
|
||||||
|
const handleContextMenuClose = () => {
|
||||||
|
contextMenuManager.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
const keyword = ref<string>('');
|
||||||
|
|
||||||
|
//#region 机器人列表
|
||||||
|
const labels = computed<RobotLabel[]>(() => editor.value.robotLabels.value ?? []);
|
||||||
|
const robots = computed<RobotInfo[]>(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value)));
|
||||||
|
const getLabelRobots = (ids: RobotLabel['robots']) =>
|
||||||
|
ids?.map((id) => editor.value.getRobotById(id)).filter((robot) => robot?.label.includes(keyword.value));
|
||||||
|
|
||||||
|
const selected = reactive<Set<RobotInfo['id']>>(new Set());
|
||||||
|
watch(
|
||||||
|
() => props.editable,
|
||||||
|
() => selected.clear(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAllSelected = computed<boolean>(() => selected.size > 0 && robots.value.every(({ id }) => selected.has(id)));
|
||||||
|
const selectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
robots.value.forEach(({ id }) => selected.add(id));
|
||||||
|
} else {
|
||||||
|
selected.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkLabelSelected = (ids: RobotLabel['robots']) => !!ids?.length && ids.every((id) => selected.has(id));
|
||||||
|
const selectLabel = (ids: RobotLabel['robots'], checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
ids?.forEach((id) => selected.add(id));
|
||||||
|
} else {
|
||||||
|
ids?.forEach((id) => selected.delete(id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectRobot = (id: RobotInfo['id'], checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
selected.add(id);
|
||||||
|
} else {
|
||||||
|
selected.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理机器人右键点击
|
||||||
|
const handleRobotRightClick = (event: MouseEvent, robot: RobotInfo) => {
|
||||||
|
// 为事件目标添加机器人标识
|
||||||
|
if (event.target && event.target instanceof HTMLElement) {
|
||||||
|
event.target.setAttribute('data-robot-id', robot.id);
|
||||||
|
event.target.setAttribute('data-robot-type', 'robot');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右键菜单 - handleContextMenu 内部会处理 preventDefault 和 stopPropagation
|
||||||
|
handleContextMenu(event, contextMenuManager, {
|
||||||
|
robotService: {
|
||||||
|
getRobotById: (id: string) => editor.value.getRobotById(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 机器人标签操作
|
||||||
|
const refAddRobot = shallowRef<RobotAddModalRef>();
|
||||||
|
const refRegisterRobot = shallowRef<RobotRegisterModalRef>();
|
||||||
|
const refRenameLabel = shallowRef<RobotLabelRenameModalRef>();
|
||||||
|
|
||||||
|
const toDeleteLabel = (id: RobotLabel['id']) =>
|
||||||
|
Modal.confirm({
|
||||||
|
class: 'confirm',
|
||||||
|
title: t('您确定要删除该机器人标签吗?'),
|
||||||
|
centered: true,
|
||||||
|
cancelText: t('返回'),
|
||||||
|
okText: t('删除'),
|
||||||
|
onOk: () => editor.value.deleteRobotLabel(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 机器人操作
|
||||||
|
const toRemoveRobot = (labelId: string, robotId: RobotInfo['id']) =>
|
||||||
|
Modal.confirm({
|
||||||
|
class: 'confirm',
|
||||||
|
title: t('您确定要从此标签中移除该机器人吗?'),
|
||||||
|
centered: true,
|
||||||
|
cancelText: t('返回'),
|
||||||
|
okText: t('移除'),
|
||||||
|
onOk: () => {
|
||||||
|
editor.value.removeRobotFromLabel(labelId, robotId);
|
||||||
|
selected.delete(robotId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toRemoveRobots = () =>
|
||||||
|
Modal.confirm({
|
||||||
|
class: 'confirm',
|
||||||
|
title: t('您确定要移除已选机器人的所有标签吗?'),
|
||||||
|
centered: true,
|
||||||
|
cancelText: t('返回'),
|
||||||
|
okText: t('移除'),
|
||||||
|
onOk: () => {
|
||||||
|
editor.value.removeRobotsFromAllLabels([...selected.keys()]);
|
||||||
|
selected.clear();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RobotAddModal ref="refAddRobot" :token="token" />
|
||||||
|
<RobotRegisterModal ref="refRegisterRobot" :token="token" />
|
||||||
|
<RobotLabelRenameModal ref="refRenameLabel" :token="token" />
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<ContextMenu
|
||||||
|
:visible="contextMenuState.visible"
|
||||||
|
:x="contextMenuState.x"
|
||||||
|
:y="contextMenuState.y"
|
||||||
|
:menu-type="contextMenuState.menuType"
|
||||||
|
:robot-id="contextMenuState.robotInfo?.id"
|
||||||
|
:token="token"
|
||||||
|
@close="handleContextMenuClose"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-flex class="full" vertical>
|
||||||
|
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
|
||||||
|
<template #suffix>
|
||||||
|
<i class="icon search size-16" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
|
||||||
|
<a-flex v-if="editable" class="mb-8" style="height: 32px" justify="space-between" align="center">
|
||||||
|
<a-checkbox :checked="isAllSelected" @change="selectAll($event.target.checked)">{{ $t('全选') }}</a-checkbox>
|
||||||
|
<a-space align="center">
|
||||||
|
<a-button
|
||||||
|
class="icon-btn panel-btn"
|
||||||
|
size="small"
|
||||||
|
:title="$t('移除机器人')"
|
||||||
|
@click="toRemoveRobots"
|
||||||
|
:disabled="!selected.size"
|
||||||
|
>
|
||||||
|
<i class="mask trash_fill" />
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-flex>
|
||||||
|
|
||||||
|
<a-collapse style="flex: auto; overflow-y: auto" expand-icon-position="end" ghost defaultActiveKey="defaultActive">
|
||||||
|
<template #expandIcon="v">
|
||||||
|
<i class="icon dropdown" :class="{ active: v?.isActive }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-collapse-panel v-for="({ id, label, robots }, index) in labels" :key="index === 0 ? 'defaultActive' : id">
|
||||||
|
<template v-if="editable" #extra>
|
||||||
|
<a-dropdown placement="bottomRight">
|
||||||
|
<a-button class="icon-btn" size="small" @click.stop>
|
||||||
|
<i class="icon dot" />
|
||||||
|
</a-button>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu>
|
||||||
|
<a-menu-item>
|
||||||
|
<a-space align="center" :size="4" @click="refAddRobot?.open(id, 'label')">
|
||||||
|
<i class="icon plus size-20" />
|
||||||
|
<span>{{ $t('添加机器人') }}</span>
|
||||||
|
</a-space>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item>
|
||||||
|
<a-space align="center" :size="4" @click="refRegisterRobot?.open(id)">
|
||||||
|
<i class="icon register size-20" />
|
||||||
|
<span>{{ $t('注册机器人') }}</span>
|
||||||
|
</a-space>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item @click="refRenameLabel?.open(id, label)">
|
||||||
|
<a-space align="center" :size="4">
|
||||||
|
<i class="icon pen size-20" />
|
||||||
|
<span>{{ $t('修改标签名称') }}</span>
|
||||||
|
</a-space>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item @click="toDeleteLabel(id)">
|
||||||
|
<a-space align="center" :size="4">
|
||||||
|
<i class="icon trash size-20" />
|
||||||
|
<span>{{ $t('删除标签') }}</span>
|
||||||
|
</a-space>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #header>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<a-space align="center" :size="8">
|
||||||
|
<a-checkbox
|
||||||
|
v-if="editable"
|
||||||
|
:checked="checkLabelSelected(robots)"
|
||||||
|
@change="selectLabel(robots, $event.target.checked)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</a-space>
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-list rowKey="id" :data-source="getLabelRobots(robots)">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<a-list-item
|
||||||
|
:class="{ 'ph-16': !editable, 'pl-12': editable, 'pr-8': editable, selected: item.id === current }"
|
||||||
|
style="height: 36px"
|
||||||
|
@click="emit('change', item.id)"
|
||||||
|
@contextmenu="handleRobotRightClick($event, item)"
|
||||||
|
>
|
||||||
|
<template v-if="editable" #actions>
|
||||||
|
<a-button
|
||||||
|
class="icon-btn panel-btn"
|
||||||
|
size="small"
|
||||||
|
:title="$t('移除机器人')"
|
||||||
|
@click.stop="toRemoveRobot(id, item.id)"
|
||||||
|
>
|
||||||
|
<i class="icon trash_fill" />
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<a-space align="center" :size="8">
|
||||||
|
<a-checkbox
|
||||||
|
v-if="editable"
|
||||||
|
:checked="selected.has(item.id)"
|
||||||
|
@change="selectRobot(item.id, $event.target.checked)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
|
||||||
|
</a-space>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
|
||||||
|
<a-button class="mt-8" v-if="editable" type="dashed" size="large" block @click="editor.createRobotLabel()">
|
||||||
|
<i class="mask plus size-20 primary" />
|
||||||
|
<span>{{ $t('添加机器人标签') }}</span>
|
||||||
|
</a-button>
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.open-label-btn {
|
||||||
|
margin-right: -8px;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
.ant-collapse-header:hover & {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 机器人列表项样式
|
||||||
|
.ant-list-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -230,7 +230,7 @@ const handleExportConfirm = async (payload: ExportConfirmPayload) => {
|
|||||||
});
|
});
|
||||||
await exportSceneToSmap(sceneFile, payload.smapFile, payload.smapFile.name.replace(/\.smap$/i, ''));
|
await exportSceneToSmap(sceneFile, payload.smapFile, payload.smapFile.name.replace(/\.smap$/i, ''));
|
||||||
} else if (payload.format === 'iray') {
|
} else if (payload.format === 'iray') {
|
||||||
// @ts-ignore
|
// @ts-expect-error iray params are not typed
|
||||||
await convertSceneToIray(sceneJson, payload.smapFile, title.value || 'unknown', payload.irayParams);
|
await convertSceneToIray(sceneJson, payload.smapFile, title.value || 'unknown', payload.irayParams);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -345,7 +345,7 @@ const handleAutoCreateStorageCancel = () => {
|
|||||||
ref="leftSiderEl"
|
ref="leftSiderEl"
|
||||||
>
|
>
|
||||||
<a-tabs type="card" v-show="!leftCollapsed">
|
<a-tabs type="card" v-show="!leftCollapsed">
|
||||||
<a-tab-pane key="1" :tab="$t('机器人')">
|
<a-tab-pane key="1" :tab="$t('机器人组')">
|
||||||
<RobotGroups
|
<RobotGroups
|
||||||
v-if="editor"
|
v-if="editor"
|
||||||
:token="EDITOR_KEY"
|
:token="EDITOR_KEY"
|
||||||
@ -356,6 +356,16 @@ const handleAutoCreateStorageCancel = () => {
|
|||||||
show-group-edit
|
show-group-edit
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="1.5" :tab="$t('机器人标签')">
|
||||||
|
<RobotLabels
|
||||||
|
v-if="editor"
|
||||||
|
:token="EDITOR_KEY"
|
||||||
|
:sid="id"
|
||||||
|
:editable="editable"
|
||||||
|
:current="current?.id"
|
||||||
|
@change="selectRobot"
|
||||||
|
/>
|
||||||
|
</a-tab-pane>
|
||||||
<a-tab-pane key="2" :tab="$t('库位')">
|
<a-tab-pane key="2" :tab="$t('库位')">
|
||||||
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-storage />
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-storage />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
type Point,
|
type Point,
|
||||||
type Rect,
|
type Rect,
|
||||||
} from '@api/map';
|
} from '@api/map';
|
||||||
import type { RobotGroup, RobotInfo, RobotType } from '@api/robot';
|
import type { RobotGroup, RobotInfo, RobotLabel,RobotType } from '@api/robot';
|
||||||
import type {
|
import type {
|
||||||
GroupSceneDetail,
|
GroupSceneDetail,
|
||||||
SceneData,
|
SceneData,
|
||||||
@ -85,7 +85,7 @@ export class EditorService extends Meta2d {
|
|||||||
scene.robotGroups = [detail.group];
|
scene.robotGroups = [detail.group];
|
||||||
scene.robots = detail.robots;
|
scene.robots = detail.robots;
|
||||||
}
|
}
|
||||||
const { robotGroups, robots, points, routes, areas, ...extraFields } = scene;
|
const { robotGroups, robots, points, routes, areas, robotLabels, ...extraFields } = scene;
|
||||||
// 保存所有额外字段(包括width、height等)
|
// 保存所有额外字段(包括width、height等)
|
||||||
this.#originalSceneData = extraFields;
|
this.#originalSceneData = extraFields;
|
||||||
|
|
||||||
@ -94,6 +94,7 @@ export class EditorService extends Meta2d {
|
|||||||
this.open();
|
this.open();
|
||||||
this.setState(editable);
|
this.setState(editable);
|
||||||
this.#loadRobots(robotGroups, robots);
|
this.#loadRobots(robotGroups, robots);
|
||||||
|
this.#loadLabels(robotLabels);
|
||||||
await this.#loadScenePoints(points, isImport);
|
await this.#loadScenePoints(points, isImport);
|
||||||
this.#loadSceneRoutes(routes, isImport);
|
this.#loadSceneRoutes(routes, isImport);
|
||||||
await this.#loadSceneAreas(areas, isImport);
|
await this.#loadSceneAreas(areas, isImport);
|
||||||
@ -123,6 +124,7 @@ export class EditorService extends Meta2d {
|
|||||||
scale,
|
scale,
|
||||||
origin: { x: x + origin.x, y: y + origin.y },
|
origin: { x: x + origin.x, y: y + origin.y },
|
||||||
robotGroups: this.robotGroups.value,
|
robotGroups: this.robotGroups.value,
|
||||||
|
robotLabels: this.robotLabels.value,
|
||||||
robots: this.robots,
|
robots: this.robots,
|
||||||
points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
|
points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
|
||||||
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
|
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
|
||||||
@ -203,6 +205,9 @@ export class EditorService extends Meta2d {
|
|||||||
robots?.forEach((v) => this.#robotMap.set(v.id, v));
|
robots?.forEach((v) => this.#robotMap.set(v.id, v));
|
||||||
this.#robotGroups$$.next(groups ?? []);
|
this.#robotGroups$$.next(groups ?? []);
|
||||||
}
|
}
|
||||||
|
#loadLabels(labels?: RobotLabel[]): void {
|
||||||
|
this.#robotLabels$$.next(labels ?? []);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 从场景数据加载点位到画布
|
* 从场景数据加载点位到画布
|
||||||
* @param points 标准场景点位数据数组
|
* @param points 标准场景点位数据数组
|
||||||
@ -616,8 +621,12 @@ export class EditorService extends Meta2d {
|
|||||||
const groups = clone(this.#robotGroups$$.value);
|
const groups = clone(this.#robotGroups$$.value);
|
||||||
groups.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
groups.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
||||||
this.#robotGroups$$.next(groups);
|
this.#robotGroups$$.next(groups);
|
||||||
|
const labels = clone(this.#robotLabels$$.value);
|
||||||
|
labels.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
||||||
|
this.#robotLabels$$.next(labels);
|
||||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
}
|
}
|
||||||
public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
|
public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
|
||||||
ids?.forEach((v) => {
|
ids?.forEach((v) => {
|
||||||
@ -631,6 +640,9 @@ export class EditorService extends Meta2d {
|
|||||||
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
||||||
public readonly robotGroups = useObservable<RobotGroup[]>(this.#robotGroups$$.pipe(debounceTime(300)));
|
public readonly robotGroups = useObservable<RobotGroup[]>(this.#robotGroups$$.pipe(debounceTime(300)));
|
||||||
|
|
||||||
|
readonly #robotLabels$$ = new BehaviorSubject<RobotLabel[]>([]);
|
||||||
|
public readonly robotLabels = useObservable<RobotLabel[]>(this.#robotLabels$$.pipe(debounceTime(300)));
|
||||||
|
|
||||||
public createRobotGroup(): void {
|
public createRobotGroup(): void {
|
||||||
const id = s8();
|
const id = s8();
|
||||||
const label = `RG${id}`;
|
const label = `RG${id}`;
|
||||||
@ -657,6 +669,92 @@ export class EditorService extends Meta2d {
|
|||||||
this.#robotGroups$$.next([...groups]);
|
this.#robotGroups$$.next([...groups]);
|
||||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||||
}
|
}
|
||||||
|
public createRobotLabel(): void {
|
||||||
|
const id = s8();
|
||||||
|
const label = `RL${id}`;
|
||||||
|
const labels = clone(this.#robotLabels$$.value);
|
||||||
|
labels.push({ id, label });
|
||||||
|
this.#robotLabels$$.next(labels);
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
|
}
|
||||||
|
public deleteRobotLabel(id: RobotLabel['id']): void {
|
||||||
|
const labels = clone(this.#robotLabels$$.value);
|
||||||
|
const labelToDelete = labels.find((v) => v.id === id);
|
||||||
|
if (!labelToDelete) return;
|
||||||
|
|
||||||
|
// Remove the label id from all robots
|
||||||
|
const robotsInLabel = labelToDelete.robots;
|
||||||
|
if (robotsInLabel) {
|
||||||
|
robotsInLabel.forEach((robotId) => {
|
||||||
|
const robot = this.getRobotById(robotId);
|
||||||
|
if (robot?.lid) {
|
||||||
|
const newLid = robot.lid.filter((lid) => lid !== id);
|
||||||
|
this.updateRobot(robot.id, { lid: newLid });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(labels, (l) => l.id === id);
|
||||||
|
this.#robotLabels$$.next(labels);
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
|
}
|
||||||
|
public updateRobotLabel(id: RobotLabel['id'], labelName: RobotLabel['label']): void {
|
||||||
|
const labels = this.#robotLabels$$.value;
|
||||||
|
const label = labels.find((v) => v.id === id);
|
||||||
|
if (isNil(label)) throw Error('未找到目标机器人标签');
|
||||||
|
if (some(labels, (l) => l.label === labelName && l.id !== id)) throw Error('机器人标签名称已经存在');
|
||||||
|
label.label = labelName;
|
||||||
|
this.#robotLabels$$.next([...labels]);
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
|
}
|
||||||
|
public addRobotsToLabel(lid: RobotLabel['id'], robots: RobotInfo[]): void {
|
||||||
|
const labels = clone(this.#robotLabels$$.value);
|
||||||
|
const label = labels.find((v) => v.id === lid);
|
||||||
|
if (isNil(label)) throw Error('未找到目标机器人标签');
|
||||||
|
label.robots ??= [];
|
||||||
|
robots.forEach((v) => {
|
||||||
|
const existingRobot = this.#robotMap.get(v.id);
|
||||||
|
if (existingRobot) {
|
||||||
|
// If robot is already in scene
|
||||||
|
if (!existingRobot.lid) {
|
||||||
|
existingRobot.lid = [];
|
||||||
|
}
|
||||||
|
if (!existingRobot.lid.includes(lid)) {
|
||||||
|
existingRobot.lid.push(lid);
|
||||||
|
}
|
||||||
|
if (label.robots && !label.robots.includes(v.id)) {
|
||||||
|
label.robots.push(v.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If robot is new to the scene
|
||||||
|
const newRobot = { ...v, lid: [lid] };
|
||||||
|
this.#robotMap.set(v.id, newRobot);
|
||||||
|
if (label.robots) {
|
||||||
|
label.robots.push(v.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#robotLabels$$.next(labels);
|
||||||
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
|
}
|
||||||
|
public removeRobotFromLabel(labelId: string, robotId: string): void {
|
||||||
|
const labels = clone(this.#robotLabels$$.value);
|
||||||
|
const label = labels.find((v) => v.id === labelId);
|
||||||
|
if (label?.robots) {
|
||||||
|
remove(label.robots, (id) => id === robotId);
|
||||||
|
}
|
||||||
|
this.#robotLabels$$.next(labels);
|
||||||
|
|
||||||
|
const robot = this.getRobotById(robotId);
|
||||||
|
if (robot?.lid) {
|
||||||
|
const newLid = robot.lid.filter((id) => id !== labelId);
|
||||||
|
this.updateRobot(robotId, { lid: newLid });
|
||||||
|
}
|
||||||
|
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
/** 保存从后台传来的所有额外字段(除了已处理的robotGroups、robots、points、routes、areas之外的字段) */
|
/** 保存从后台传来的所有额外字段(除了已处理的robotGroups、robots、points、routes、areas之外的字段) */
|
||||||
@ -2051,6 +2149,26 @@ export class EditorService extends Meta2d {
|
|||||||
this.addDrawLineFn('bezier2', lineBezier2);
|
this.addDrawLineFn('bezier2', lineBezier2);
|
||||||
this.addDrawLineFn('bezier3', lineBezier3);
|
this.addDrawLineFn('bezier3', lineBezier3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeRobotsFromAllLabels(robotIds: string[]): void {
|
||||||
|
robotIds.forEach((robotId) => {
|
||||||
|
const robot = this.getRobotById(robotId);
|
||||||
|
if (robot) {
|
||||||
|
this.updateRobot(robotId, { lid: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = clone(this.#robotLabels$$.value);
|
||||||
|
labels.forEach((label) => {
|
||||||
|
if (label.robots) {
|
||||||
|
remove(label.robots, (id) => robotIds.includes(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#robotLabels$$.next(labels);
|
||||||
|
|
||||||
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||||
|
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 自定义绘制函数
|
//#region 自定义绘制函数
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user