feat(context-menu): 新增站点右键菜单功能,支持通过机器人选择弹窗为站点下发导航任务
This commit is contained in:
parent
18d4205b74
commit
a6c642fef4
@ -8,6 +8,7 @@ const enum API {
|
|||||||
手动发布充电任务 = '/amr/manuallSetCharging',
|
手动发布充电任务 = '/amr/manuallSetCharging',
|
||||||
暂停AMR = '/amr',
|
暂停AMR = '/amr',
|
||||||
恢复AMR = '/amr',
|
恢复AMR = '/amr',
|
||||||
|
手动移动任务 = '/task/manual/move',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只保留右键菜单需要的API函数
|
// 只保留右键菜单需要的API函数
|
||||||
@ -127,3 +128,58 @@ export async function resumeAmr(amrId: string, sceneId: string): Promise<boolean
|
|||||||
return false;
|
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"
|
@custom-image="handleCustomImage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 站点菜单 -->
|
||||||
|
<PointMenu
|
||||||
|
v-else-if="menuType === 'point'"
|
||||||
|
:menu-type="menuType"
|
||||||
|
:point-info="pointInfo"
|
||||||
|
@action-complete="handleActionComplete"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 默认菜单 -->
|
<!-- 默认菜单 -->
|
||||||
<DefaultMenu v-else @action-complete="handleActionComplete" />
|
<DefaultMenu v-else @action-complete="handleActionComplete" />
|
||||||
</div>
|
</div>
|
||||||
@ -38,6 +46,7 @@ import { computed, ref, watch } from 'vue';
|
|||||||
|
|
||||||
import type { StorageLocationInfo } from '../../services/context-menu';
|
import type { StorageLocationInfo } from '../../services/context-menu';
|
||||||
import DefaultMenu from './default-menu.vue';
|
import DefaultMenu from './default-menu.vue';
|
||||||
|
import PointMenu from './point-menu.vue';
|
||||||
import RobotMenu from './robot-menu.vue';
|
import RobotMenu from './robot-menu.vue';
|
||||||
import StorageMenu from './storage-menu.vue';
|
import StorageMenu from './storage-menu.vue';
|
||||||
|
|
||||||
@ -48,6 +57,11 @@ interface Props {
|
|||||||
menuType?: 'default' | 'storage' | 'storage-background' | 'robot' | 'point' | 'area';
|
menuType?: 'default' | 'storage' | 'storage-background' | 'robot' | 'point' | 'area';
|
||||||
storageLocations?: StorageLocationInfo[];
|
storageLocations?: StorageLocationInfo[];
|
||||||
robotId?: string; // 改为传递机器人ID
|
robotId?: string; // 改为传递机器人ID
|
||||||
|
pointInfo?: { // 添加站点信息
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
apiBaseUrl?: string;
|
apiBaseUrl?: string;
|
||||||
isPlaybackMode?: boolean;
|
isPlaybackMode?: boolean;
|
||||||
}
|
}
|
||||||
@ -67,6 +81,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
});
|
});
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 拖拽功能
|
// 拖拽功能
|
||||||
const menuRef = ref<HTMLElement | null>(null);
|
const menuRef = ref<HTMLElement | null>(null);
|
||||||
const handleRef = ref<HTMLElement | null>(null);
|
const handleRef = ref<HTMLElement | null>(null);
|
||||||
@ -125,27 +141,8 @@ defineOptions({
|
|||||||
name: 'ContextMenu',
|
name: 'ContextMenu',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 触发器引用
|
|
||||||
const triggerRef = ref<HTMLElement | null>(null);
|
|
||||||
const mainMenuWidth = ref(200); // 默认宽度
|
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(() => {
|
const headerTitle = computed(() => {
|
||||||
if (props.menuType === 'storage-background') return '库位状态';
|
if (props.menuType === 'storage-background') return '库位状态';
|
||||||
@ -156,12 +153,6 @@ const headerTitle = computed(() => {
|
|||||||
return '右键菜单';
|
return '右键菜单';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理下拉菜单开关变化
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
|
||||||
if (!open) {
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理操作完成事件
|
// 处理操作完成事件
|
||||||
const handleActionComplete = (data: { success: boolean; action: string; message?: string }) => {
|
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"
|
:menu-type="contextMenuState.menuType"
|
||||||
:storage-locations="contextMenuState.storageLocations"
|
:storage-locations="contextMenuState.storageLocations"
|
||||||
:robot-id="contextMenuState.robotInfo?.id"
|
:robot-id="contextMenuState.robotInfo?.id"
|
||||||
|
:point-info="contextMenuState.pointInfo"
|
||||||
:token="EDITOR_KEY"
|
:token="EDITOR_KEY"
|
||||||
:is-playback-mode="isPlaybackMode"
|
:is-playback-mode="isPlaybackMode"
|
||||||
@close="handleCloseContextMenu"
|
@close="handleCloseContextMenu"
|
||||||
|
|||||||
@ -64,9 +64,7 @@ function parsePenObject(pen: Record<string, unknown>, position: { x: number; y:
|
|||||||
locked: boolean;
|
locked: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('解析后的数据:', { id, name, tags, storageLocation });
|
|
||||||
|
|
||||||
const target = document.elementFromPoint(position.x, position.y) as HTMLElement;
|
const target = document.elementFromPoint(position.x, position.y) as HTMLElement;
|
||||||
|
|
||||||
// 根据tags判断类型 - 统一处理库位相关区域
|
// 根据tags判断类型 - 统一处理库位相关区域
|
||||||
|
|||||||
@ -5,19 +5,10 @@
|
|||||||
|
|
||||||
import type { ParsedEventData } from './event-parser';
|
import type { ParsedEventData } from './event-parser';
|
||||||
import { parseEventData, parsePenData } 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 type { RobotMenuConfig } from './robot-menu.service';
|
||||||
import { getStorageMenuConfig, type StorageMenuConfig } from './storage-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 {
|
export interface AreaMenuConfig {
|
||||||
menuType: 'area' | 'default';
|
menuType: 'area' | 'default';
|
||||||
areaInfo?: {
|
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) {
|
switch (type) {
|
||||||
case 'storage-background':
|
case 'storage-background':
|
||||||
case 'storage':
|
case 'storage':
|
||||||
case 'point':
|
// 库位相关类型
|
||||||
// 库位相关类型,包括点区域(如果是动作点且有库位信息)
|
|
||||||
return getStorageMenuConfig(type, data, services?.storageLocationService);
|
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': {
|
case 'robot': {
|
||||||
// 机器人类型 - 直接获取机器人信息
|
// 机器人类型 - 直接获取机器人信息
|
||||||
const robotInfo = services?.robotService?.getRobotById?.(data.id);
|
const robotInfo = services?.robotService?.getRobotById?.(data.id);
|
||||||
@ -104,16 +112,16 @@ function processContextMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
manager.setState({
|
const stateToSet = {
|
||||||
visible: true,
|
visible: true,
|
||||||
x: parsedData.position.x,
|
x: parsedData.position.x,
|
||||||
y: parsedData.position.y,
|
y: parsedData.position.y,
|
||||||
eventData: parsedData,
|
eventData: parsedData,
|
||||||
isRightClickActive: true, // 标记右键菜单正在显示
|
isRightClickActive: true, // 标记右键菜单正在显示
|
||||||
...menuConfig, // 展开具体配置
|
...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;
|
eventData?: any;
|
||||||
storageLocations?: any[];
|
storageLocations?: any[];
|
||||||
robotInfo?: any;
|
robotInfo?: any;
|
||||||
|
pointInfo?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
isRightClickActive?: boolean; // 新增:标记右键菜单是否正在显示,用于阻止选中状态更新
|
isRightClickActive?: boolean; // 新增:标记右键菜单是否正在显示,用于阻止选中状态更新
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user