feat(context-menu): 新增站点右键菜单功能,支持通过机器人选择弹窗为站点下发导航任务
This commit is contained in:
parent
18d4205b74
commit
a6c642fef4
@ -8,6 +8,7 @@ const enum API {
|
||||
手动发布充电任务 = '/amr/manuallSetCharging',
|
||||
暂停AMR = '/amr',
|
||||
恢复AMR = '/amr',
|
||||
手动移动任务 = '/task/manual/move',
|
||||
}
|
||||
|
||||
// 只保留右键菜单需要的API函数
|
||||
@ -127,3 +128,58 @@ export async function resumeAmr(amrId: string, sceneId: string): Promise<boolean
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动下发小车移动任务
|
||||
* @param params 任务参数
|
||||
* @returns 任务执行结果
|
||||
*/
|
||||
export async function manualMoveTask(params: {
|
||||
sceneId: string;
|
||||
amrId: string;
|
||||
taskName: string;
|
||||
targetStationName: string;
|
||||
description?: string;
|
||||
operator?: string;
|
||||
priority: number;
|
||||
}): Promise<{ success: boolean; message: string; data?: any }> {
|
||||
type B = {
|
||||
sceneId: string;
|
||||
amrId: string;
|
||||
taskName: string;
|
||||
targetStationName: string;
|
||||
description?: string;
|
||||
operator?: string;
|
||||
priority: number;
|
||||
};
|
||||
type D = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
code: number;
|
||||
result?: {
|
||||
taskId: string;
|
||||
taskBlockId: string;
|
||||
vwedTaskId: string;
|
||||
amrId: string;
|
||||
amrName: string;
|
||||
currentStationName: string;
|
||||
targetStationName: string;
|
||||
};
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await http.post<D, B>(API.手动移动任务, params);
|
||||
return {
|
||||
success: (response as any).success,
|
||||
message: (response as any).message,
|
||||
data: (response as any).result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('手动下发移动任务失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: '', // 让全局错误处理显示具体错误
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,14 @@
|
||||
@custom-image="handleCustomImage"
|
||||
/>
|
||||
|
||||
<!-- 站点菜单 -->
|
||||
<PointMenu
|
||||
v-else-if="menuType === 'point'"
|
||||
:menu-type="menuType"
|
||||
:point-info="pointInfo"
|
||||
@action-complete="handleActionComplete"
|
||||
/>
|
||||
|
||||
<!-- 默认菜单 -->
|
||||
<DefaultMenu v-else @action-complete="handleActionComplete" />
|
||||
</div>
|
||||
@ -38,6 +46,7 @@ import { computed, ref, watch } from 'vue';
|
||||
|
||||
import type { StorageLocationInfo } from '../../services/context-menu';
|
||||
import DefaultMenu from './default-menu.vue';
|
||||
import PointMenu from './point-menu.vue';
|
||||
import RobotMenu from './robot-menu.vue';
|
||||
import StorageMenu from './storage-menu.vue';
|
||||
|
||||
@ -48,6 +57,11 @@ interface Props {
|
||||
menuType?: 'default' | 'storage' | 'storage-background' | 'robot' | 'point' | 'area';
|
||||
storageLocations?: StorageLocationInfo[];
|
||||
robotId?: string; // 改为传递机器人ID
|
||||
pointInfo?: { // 添加站点信息
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
apiBaseUrl?: string;
|
||||
isPlaybackMode?: boolean;
|
||||
}
|
||||
@ -67,6 +81,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
|
||||
|
||||
// 拖拽功能
|
||||
const menuRef = ref<HTMLElement | null>(null);
|
||||
const handleRef = ref<HTMLElement | null>(null);
|
||||
@ -125,27 +141,8 @@ defineOptions({
|
||||
name: 'ContextMenu',
|
||||
});
|
||||
|
||||
// 触发器引用
|
||||
const triggerRef = ref<HTMLElement | null>(null);
|
||||
const mainMenuWidth = ref(200); // 默认宽度
|
||||
|
||||
// 触发器样式 - 创建一个不可见的定位点
|
||||
const triggerStyle = computed(() => ({
|
||||
position: 'fixed' as const,
|
||||
left: `${props.x}px`,
|
||||
top: `${props.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
pointerEvents: 'none' as const,
|
||||
zIndex: 9999,
|
||||
}));
|
||||
|
||||
// 使用 Ant Design 的自动边界检测,无需手动计算
|
||||
const dropdownPlacement = 'bottomLeft' as const;
|
||||
|
||||
// 获取弹出层容器
|
||||
const getPopupContainer = () => document.body;
|
||||
|
||||
// 动态标题
|
||||
const headerTitle = computed(() => {
|
||||
if (props.menuType === 'storage-background') return '库位状态';
|
||||
@ -156,12 +153,6 @@ const headerTitle = computed(() => {
|
||||
return '右键菜单';
|
||||
});
|
||||
|
||||
// 处理下拉菜单开关变化
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理操作完成事件
|
||||
const handleActionComplete = (data: { success: boolean; action: string; message?: string }) => {
|
||||
|
||||
256
src/components/context-menu/point-menu.vue
Normal file
256
src/components/context-menu/point-menu.vue
Normal file
@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="point-menu">
|
||||
<!-- 菜单头部:显示站点信息 -->
|
||||
<div class="menu-header">
|
||||
<div class="point-info">
|
||||
<div class="point-icon">📍</div>
|
||||
<div class="point-details">
|
||||
<div class="point-name">{{ pointInfo?.name || '未知站点' }}</div>
|
||||
<div class="point-id">ID: {{ pointInfo?.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 菜单分割线 -->
|
||||
<a-divider style="margin: 12px 0;" />
|
||||
|
||||
<!-- 菜单选项 -->
|
||||
<div class="menu-options">
|
||||
<!-- 导航至此站点 -->
|
||||
<div
|
||||
class="menu-item"
|
||||
:class="{ disabled: !canNavigate }"
|
||||
@click="handleNavigateToPoint"
|
||||
>
|
||||
<div class="menu-item-content">
|
||||
<span class="menu-icon">🧭</span>
|
||||
<span class="menu-text">导航至此站点</span>
|
||||
</div>
|
||||
<div class="menu-description">
|
||||
选择机器人并下发移动任务到当前站点
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机器人选择弹窗 -->
|
||||
<RobotSelectorModal
|
||||
v-model:open="showRobotSelector"
|
||||
:target-point-name="pointInfo?.name"
|
||||
@confirm="handleRobotSelected"
|
||||
@cancel="handleRobotSelectorCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import type { RobotInfo } from '../../apis/robot';
|
||||
import { executeNavigateToPoint } from '../../services/context-menu/point-menu.service';
|
||||
import RobotSelectorModal from '../modal/robot-selector-modal.vue';
|
||||
|
||||
interface Props {
|
||||
menuType?: 'point' | 'default';
|
||||
pointInfo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'actionComplete', data: {
|
||||
action: string;
|
||||
point: any;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
|
||||
// 响应式数据
|
||||
const showRobotSelector = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 计算属性
|
||||
const pointInfo = computed(() => props.pointInfo);
|
||||
|
||||
// 判断是否可以进行导航
|
||||
const canNavigate = computed(() => {
|
||||
return pointInfo.value?.name && pointInfo.value?.id;
|
||||
});
|
||||
|
||||
// 处理导航到站点
|
||||
const handleNavigateToPoint = async () => {
|
||||
if (!canNavigate.value) {
|
||||
message.warning('站点信息不完整,无法导航');
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开机器人选择弹窗
|
||||
showRobotSelector.value = true;
|
||||
};
|
||||
|
||||
// 处理机器人选择
|
||||
const handleRobotSelected = async (data: { robot: RobotInfo; targetPointName: string }) => {
|
||||
if (!pointInfo.value) {
|
||||
message.error('站点信息丢失,无法执行导航');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
console.log('选择机器人进行导航:', {
|
||||
robot: data.robot.label,
|
||||
targetPoint: data.targetPointName,
|
||||
});
|
||||
|
||||
// 调用导航API
|
||||
const result = await executeNavigateToPoint(
|
||||
pointInfo.value.id,
|
||||
pointInfo.value.name,
|
||||
data.robot.id,
|
||||
data.robot.label
|
||||
);
|
||||
|
||||
// 发送操作完成事件
|
||||
emit('actionComplete', {
|
||||
action: 'navigateToPoint',
|
||||
point: pointInfo.value,
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
});
|
||||
|
||||
// 显示结果提示
|
||||
if (result.success) {
|
||||
message.success(`已为机器人 ${data.robot.label} 下发导航任务到 ${data.targetPointName}`);
|
||||
} else {
|
||||
// 如果API返回了错误信息,让全局错误处理显示
|
||||
if (!result.message) {
|
||||
message.error('导航任务下发失败');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('导航操作失败:', error);
|
||||
|
||||
// 发送失败事件
|
||||
emit('actionComplete', {
|
||||
action: 'navigateToPoint',
|
||||
point: pointInfo.value,
|
||||
success: false,
|
||||
message: undefined, // 让全局错误处理显示
|
||||
});
|
||||
|
||||
message.error('导航操作失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理机器人选择弹窗取消
|
||||
const handleRobotSelectorCancel = () => {
|
||||
showRobotSelector.value = false;
|
||||
console.log('用户取消了机器人选择');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.point-menu {
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 菜单头部 */
|
||||
.menu-header {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.point-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.point-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.point-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.point-id {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 菜单选项 */
|
||||
.menu-options {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 12px;
|
||||
margin: 4px 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.menu-item:hover:not(.disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.menu-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.menu-description {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
/* 分割线样式 */
|
||||
:deep(.ant-divider) {
|
||||
border-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
280
src/components/modal/robot-selector-modal.vue
Normal file
280
src/components/modal/robot-selector-modal.vue
Normal file
@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="选择机器人"
|
||||
:width="600"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="robot-selector">
|
||||
<!-- 机器人列表 -->
|
||||
<div class="robot-list">
|
||||
<a-radio-group v-model:value="selectedRobotId" class="radio-group">
|
||||
<div
|
||||
v-for="robot in robotList"
|
||||
:key="robot.id"
|
||||
class="robot-item"
|
||||
:class="{ disabled: !isRobotSelectable(robot) }"
|
||||
>
|
||||
<a-radio :value="robot.id" :disabled="!isRobotSelectable(robot)">
|
||||
<div class="robot-info">
|
||||
<div class="robot-name">{{ robot.label }}</div>
|
||||
<div class="robot-details">
|
||||
<a-tag :color="getRobotStatusColor(robot.state)">
|
||||
{{ getRobotStatusText(robot.state) }}
|
||||
</a-tag>
|
||||
<span class="robot-id">ID: {{ robot.id }}</span>
|
||||
<span v-if="robot.targetPoint" class="current-station">
|
||||
目标位置: {{ robot.targetPoint }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!selectedRobotId"
|
||||
:loading="loading"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确定
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import type { RobotInfo } from '../../apis/robot';
|
||||
import { RobotState } from '../../apis/robot';
|
||||
import { getAllRobots } from '../../apis/robot';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
targetPointName?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', value: boolean): void;
|
||||
(e: 'confirm', data: { robot: RobotInfo; targetPointName: string }): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
targetPointName: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false);
|
||||
const robotList = ref<RobotInfo[]>([]);
|
||||
const selectedRobotId = ref<string>('');
|
||||
const loading = ref(false);
|
||||
|
||||
// 计算属性
|
||||
const selectedRobot = computed(() =>
|
||||
robotList.value.find(robot => robot.id === selectedRobotId.value)
|
||||
);
|
||||
|
||||
// 监听 props.open 变化
|
||||
watch(() => props.open, (newVal) => {
|
||||
visible.value = newVal;
|
||||
if (newVal) {
|
||||
loadRobotList();
|
||||
selectedRobotId.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:open', newVal);
|
||||
});
|
||||
|
||||
// 加载机器人列表
|
||||
const loadRobotList = async () => {
|
||||
try {
|
||||
const robots = await getAllRobots();
|
||||
robotList.value = robots || [];
|
||||
} catch (error) {
|
||||
console.error('加载机器人列表失败:', error);
|
||||
robotList.value = [];
|
||||
message.error('加载机器人列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 判断机器人是否可选择
|
||||
const isRobotSelectable = (robot: RobotInfo): boolean => {
|
||||
// 根据接口文档要求,机器人必须满足以下条件:
|
||||
// 1. 机器人必须在线 (isConnected === true)
|
||||
// 2. 机器人必须开启接单 (canOrder === true)
|
||||
// 3. 机器人状态必须为空闲 (state === RobotState.空闲中)
|
||||
// 4. 机器人当前没有正在执行的任务(通过目标点位判断)
|
||||
|
||||
return (
|
||||
robot.isConnected === true &&
|
||||
robot.canOrder === true &&
|
||||
robot.state === RobotState.空闲中 &&
|
||||
!robot.targetPoint // 没有目标点位表示没有正在执行的任务
|
||||
);
|
||||
};
|
||||
|
||||
// 获取机器人状态文本
|
||||
const getRobotStatusText = (state?: RobotState): string => {
|
||||
const stateMap: Record<RobotState, string> = {
|
||||
[RobotState.未知]: '未知',
|
||||
[RobotState.任务执行中]: '任务执行中',
|
||||
[RobotState.充电中]: '充电中',
|
||||
[RobotState.停靠中]: '停靠中',
|
||||
[RobotState.空闲中]: '空闲中',
|
||||
};
|
||||
return stateMap[state || RobotState.空闲中] || '未知';
|
||||
};
|
||||
|
||||
// 获取机器人状态颜色
|
||||
const getRobotStatusColor = (state?: RobotState): string => {
|
||||
const colorMap: Record<RobotState, string> = {
|
||||
[RobotState.未知]: '#d9d9d9', // 灰色 - 未知
|
||||
[RobotState.任务执行中]: '#1890ff', // 蓝色 - 忙碌
|
||||
[RobotState.充电中]: '#faad14', // 橙色 - 充电
|
||||
[RobotState.停靠中]: '#52c41a', // 绿色 - 正常
|
||||
[RobotState.空闲中]: '#52c41a', // 绿色 - 正常
|
||||
};
|
||||
return colorMap[state || RobotState.空闲中] || '#d9d9d9';
|
||||
};
|
||||
|
||||
// 处理确定按钮点击
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedRobot.value) {
|
||||
message.warning('请选择一个机器人');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.targetPointName) {
|
||||
message.error('目标站点名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 触发确认事件
|
||||
emit('confirm', {
|
||||
robot: selectedRobot.value,
|
||||
targetPointName: props.targetPointName,
|
||||
});
|
||||
|
||||
// 关闭弹窗
|
||||
visible.value = false;
|
||||
} catch (error) {
|
||||
console.error('确认操作失败:', error);
|
||||
message.error('确认操作失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理取消按钮点击
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
if (props.open) {
|
||||
loadRobotList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.robot-selector {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.robot-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.robot-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.robot-item:hover:not(.disabled) {
|
||||
border-color: #1890ff;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.robot-item.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.robot-info {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.robot-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.robot-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.robot-id {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.current-station {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 自定义单选框样式 */
|
||||
:deep(.ant-radio-wrapper) {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-radio-wrapper span:last-child) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -855,6 +855,7 @@ const handleGlobalKeydown = (event: KeyboardEvent) => {
|
||||
:menu-type="contextMenuState.menuType"
|
||||
:storage-locations="contextMenuState.storageLocations"
|
||||
:robot-id="contextMenuState.robotInfo?.id"
|
||||
:point-info="contextMenuState.pointInfo"
|
||||
:token="EDITOR_KEY"
|
||||
:is-playback-mode="isPlaybackMode"
|
||||
@close="handleCloseContextMenu"
|
||||
|
||||
@ -64,9 +64,7 @@ function parsePenObject(pen: Record<string, unknown>, position: { x: number; y:
|
||||
locked: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
console.log('解析后的数据:', { id, name, tags, storageLocation });
|
||||
|
||||
|
||||
const target = document.elementFromPoint(position.x, position.y) as HTMLElement;
|
||||
|
||||
// 根据tags判断类型 - 统一处理库位相关区域
|
||||
|
||||
@ -5,19 +5,10 @@
|
||||
|
||||
import type { ParsedEventData } from './event-parser';
|
||||
import { parseEventData, parsePenData } from './event-parser';
|
||||
import { getPointMenuConfig, isStationPoint, type PointMenuConfig } from './point-menu.service';
|
||||
import type { RobotMenuConfig } from './robot-menu.service';
|
||||
import { getStorageMenuConfig, type StorageMenuConfig } from './storage-menu.service';
|
||||
|
||||
export interface PointMenuConfig {
|
||||
menuType: 'point' | 'default';
|
||||
pointInfo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
hasStorage: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AreaMenuConfig {
|
||||
menuType: 'area' | 'default';
|
||||
areaInfo?: {
|
||||
@ -27,7 +18,7 @@ export interface AreaMenuConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export type MenuConfig = StorageMenuConfig | RobotMenuConfig | PointMenuConfig | AreaMenuConfig;
|
||||
export type MenuConfig = StorageMenuConfig | RobotMenuConfig | AreaMenuConfig | PointMenuConfig;
|
||||
|
||||
/**
|
||||
* 获取菜单配置 - 统一入口
|
||||
@ -47,10 +38,27 @@ export function getMenuConfig(
|
||||
switch (type) {
|
||||
case 'storage-background':
|
||||
case 'storage':
|
||||
case 'point':
|
||||
// 库位相关类型,包括点区域(如果是动作点且有库位信息)
|
||||
// 库位相关类型
|
||||
return getStorageMenuConfig(type, data, services?.storageLocationService);
|
||||
|
||||
case 'point':
|
||||
// 检查是否为站点(动作点)而不是库位点
|
||||
if (isStationPoint(data.tags || [], data.pen)) {
|
||||
// 是站点,显示站点菜单
|
||||
// 尝试从 pen 对象中获取真实的站点名称
|
||||
const pen = data.pen as any;
|
||||
const stationName = pen?.label || pen?.text || pen?.title || data.name || `站点-${data.id}`;
|
||||
|
||||
return getPointMenuConfig({
|
||||
id: data.id || '',
|
||||
name: stationName,
|
||||
type: data.type || 'point',
|
||||
});
|
||||
} else {
|
||||
// 是库位点,显示库位菜单
|
||||
return getStorageMenuConfig(type, data, services?.storageLocationService);
|
||||
}
|
||||
|
||||
case 'robot': {
|
||||
// 机器人类型 - 直接获取机器人信息
|
||||
const robotInfo = services?.robotService?.getRobotById?.(data.id);
|
||||
@ -104,16 +112,16 @@ function processContextMenu(
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
manager.setState({
|
||||
const stateToSet = {
|
||||
visible: true,
|
||||
x: parsedData.position.x,
|
||||
y: parsedData.position.y,
|
||||
eventData: parsedData,
|
||||
isRightClickActive: true, // 标记右键菜单正在显示
|
||||
...menuConfig, // 展开具体配置
|
||||
});
|
||||
};
|
||||
|
||||
console.log('右键菜单事件数据:', parsedData);
|
||||
manager.setState(stateToSet);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
108
src/services/context-menu/point-menu.service.ts
Normal file
108
src/services/context-menu/point-menu.service.ts
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 站点右键菜单服务
|
||||
* 处理站点导航功能
|
||||
*/
|
||||
|
||||
import * as AmrApi from '../../apis/amr';
|
||||
import { editorStore } from '../../stores/editor.store';
|
||||
|
||||
export interface PointMenuConfig {
|
||||
menuType: 'point' | 'default';
|
||||
pointInfo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行路径导航任务
|
||||
* @param pointId 目标站点ID
|
||||
* @param pointName 目标站点名称
|
||||
* @param amrId 选择的机器人ID
|
||||
* @param amrName 选择的机器人名称
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export async function executeNavigateToPoint(
|
||||
pointId: string,
|
||||
pointName: string,
|
||||
amrId: string,
|
||||
amrName: string
|
||||
): Promise<{ success: boolean; message: string; result?: any }> {
|
||||
try {
|
||||
console.log(`执行路径导航: 机器人 ${amrName} 到站点 ${pointName}`);
|
||||
|
||||
// 获取场景ID
|
||||
const sceneId = editorStore.getEditorValue()?.getSceneId();
|
||||
if (!sceneId) {
|
||||
console.error('路径导航失败: 场景ID未提供');
|
||||
return {
|
||||
success: false,
|
||||
message: '路径导航失败: 场景ID未提供',
|
||||
};
|
||||
}
|
||||
|
||||
// 调用手动下发小车移动任务接口
|
||||
const result = await AmrApi.manualMoveTask({
|
||||
sceneId,
|
||||
amrId, // 注意:根据接口文档,这里应该传递AMR的ID
|
||||
taskName: '手动移动任务',
|
||||
targetStationName: pointName, // 使用站点名称作为目标站点
|
||||
description: `手动下发的小车移动任务:从当前位置到 ${pointName}`,
|
||||
operator: 'system',
|
||||
priority: 500,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
message: '路径导航任务下发成功',
|
||||
result: result.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: result.message || '路径导航任务下发失败',
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('路径导航失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: '', // 让全局错误处理显示具体错误
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站点菜单配置
|
||||
* @param pointInfo 站点信息
|
||||
* @returns 站点菜单配置
|
||||
*/
|
||||
export function getPointMenuConfig(pointInfo: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}): PointMenuConfig {
|
||||
return {
|
||||
menuType: 'point',
|
||||
pointInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为站点(动作点)
|
||||
* @param tags 元素标签
|
||||
* @param pen 元素数据
|
||||
* @returns 是否为站点
|
||||
*/
|
||||
export function isStationPoint(tags: string[], pen?: Record<string, unknown>): boolean {
|
||||
// 如果标签包含 point 且不包含 storage 相关标签,则认为是站点
|
||||
// 注意:不依赖 name 字段,因为 name 可能是 'point'
|
||||
return tags.includes('point') &&
|
||||
!tags.includes('storage') &&
|
||||
!tags.includes('storage-background') &&
|
||||
!tags.includes('storage-location') &&
|
||||
!(pen as any)?.storageLocation; // 检查是否有库位关联
|
||||
}
|
||||
@ -11,6 +11,11 @@ export interface ContextMenuState {
|
||||
eventData?: any;
|
||||
storageLocations?: any[];
|
||||
robotInfo?: any;
|
||||
pointInfo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
isRightClickActive?: boolean; // 新增:标记右键菜单是否正在显示,用于阻止选中状态更新
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user