309 lines
7.4 KiB
Vue
309 lines
7.4 KiB
Vue
<template>
|
||
<a-dropdown
|
||
v-if="visible"
|
||
:open="visible"
|
||
:trigger="[]"
|
||
:placement="dropdownPlacement"
|
||
:get-popup-container="getPopupContainer"
|
||
@open-change="handleOpenChange"
|
||
:z-index="998"
|
||
>
|
||
<div
|
||
ref="triggerRef"
|
||
class="context-menu-trigger"
|
||
:style="triggerStyle"
|
||
/>
|
||
<template #overlay>
|
||
<div class="context-menu-overlay">
|
||
<div class="context-menu-header">
|
||
<div class="menu-title">{{ headerTitle }}</div>
|
||
<div class="close-button" @click="handleCloseMenu" title="关闭菜单">
|
||
<span class="close-icon">×</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 库位菜单 -->
|
||
<StorageMenu
|
||
v-if="menuType === 'storage-background' && storageLocations?.length"
|
||
:storage-locations="storageLocations"
|
||
:menu-x="x"
|
||
:menu-y="y"
|
||
:main-menu-width="mainMenuWidth"
|
||
@action-complete="handleActionComplete"
|
||
/>
|
||
|
||
<!-- 机器人菜单 -->
|
||
<RobotMenu
|
||
v-else-if="menuType === 'robot' && robotId"
|
||
:robot-id="robotId"
|
||
:token="token"
|
||
@action-complete="handleActionComplete"
|
||
@custom-image="handleCustomImage"
|
||
/>
|
||
|
||
<!-- 默认菜单 -->
|
||
<DefaultMenu
|
||
v-else
|
||
@action-complete="handleActionComplete"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</a-dropdown>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { message } from 'ant-design-vue';
|
||
import { computed, defineAsyncComponent, type InjectionKey, ref, type ShallowRef } from 'vue';
|
||
|
||
import type { StorageLocationInfo } from '../../services/context-menu';
|
||
import type { EditorService } from '../../services/editor.service';
|
||
|
||
// 使用动态导入避免 TypeScript 错误
|
||
const DefaultMenu = defineAsyncComponent(() => import('./default-menu.vue'));
|
||
const RobotMenu = defineAsyncComponent(() => import('./robot-menu.vue'));
|
||
const StorageMenu = defineAsyncComponent(() => import('./storage-menu.vue'));
|
||
|
||
interface Props {
|
||
visible: boolean;
|
||
x?: number;
|
||
y?: number;
|
||
menuType?: 'default' | 'storage' | 'storage-background' | 'robot' | 'point' | 'area';
|
||
storageLocations?: StorageLocationInfo[];
|
||
robotId?: string; // 改为传递机器人ID
|
||
token?: InjectionKey<ShallowRef<EditorService>>; // 添加 editor token
|
||
apiBaseUrl?: string;
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'close'): void;
|
||
(e: 'actionComplete', data: any): void;
|
||
(e: 'customImage', data: { robotInfo: any }): void; // 使用 any 类型,因为 RobotInfo 不再直接导入
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
x: 0,
|
||
y: 0,
|
||
menuType: 'default',
|
||
storageLocations: () => [],
|
||
apiBaseUrl: '',
|
||
});
|
||
const emit = defineEmits<Emits>();
|
||
|
||
// 定义组件名称
|
||
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 '库位状态';
|
||
if (props.menuType === 'storage') return '库位信息';
|
||
if (props.menuType === 'robot') return '机器人操作';
|
||
if (props.menuType === 'point') return '点位操作';
|
||
if (props.menuType === 'area') return '区域操作';
|
||
return '右键菜单';
|
||
});
|
||
|
||
// 处理下拉菜单开关变化
|
||
const handleOpenChange = (open: boolean) => {
|
||
if (!open) {
|
||
emit('close');
|
||
}
|
||
};
|
||
|
||
// 处理操作完成事件
|
||
const handleActionComplete = (data: any) => {
|
||
console.log('菜单操作完成:', data);
|
||
|
||
// 根据操作结果显示相应的提示消息
|
||
if (data.success) {
|
||
const actionName = getActionDisplayName(data.action);
|
||
message.success(`${actionName}操作成功`);
|
||
} else {
|
||
const actionName = getActionDisplayName(data.action);
|
||
message.error(`${actionName}操作失败`);
|
||
}
|
||
|
||
emit('actionComplete', data);
|
||
// 所有操作都不关闭菜单,只有关闭按钮才能关闭
|
||
};
|
||
|
||
/**
|
||
* 获取操作显示名称
|
||
* @param action 操作类型
|
||
* @returns 显示名称
|
||
*/
|
||
const getActionDisplayName = (action: string): string => {
|
||
const actionMap: Record<string, string> = {
|
||
// 机器人操作
|
||
'seize_control': '抢占控制权',
|
||
'enable_orders': '可接单',
|
||
'disable_orders': '不可接单',
|
||
'pause': '暂停',
|
||
'resume': '继续',
|
||
'go_charge': '前往充电',
|
||
'go_dock': '前往停靠',
|
||
'navigate': '路径导航',
|
||
'start': '启动',
|
||
'stop': '停止',
|
||
'reset': '重置',
|
||
'diagnose': '诊断',
|
||
'update': '更新',
|
||
'custom_image': '自定义图片',
|
||
|
||
// 库位操作
|
||
'lock': '锁定',
|
||
'unlock': '解锁',
|
||
'disable': '禁用',
|
||
'enable': '启用',
|
||
'occupy': '占用',
|
||
'release': '释放',
|
||
'set_empty_tray': '设置空托盘',
|
||
'clear_empty_tray': '清除空托盘',
|
||
|
||
// 默认操作
|
||
'refresh': '刷新',
|
||
'view_info': '查看信息',
|
||
'settings': '设置',
|
||
};
|
||
|
||
return actionMap[action] || action;
|
||
};
|
||
|
||
// 处理自定义图片事件
|
||
const handleCustomImage = (data: { robotInfo: any }) => {
|
||
console.log('打开自定义图片设置:', data.robotInfo);
|
||
emit('customImage', data);
|
||
// 不关闭菜单,只有关闭按钮才能关闭
|
||
};
|
||
|
||
// 处理关闭按钮点击
|
||
const handleCloseMenu = () => {
|
||
console.log('用户点击关闭按钮');
|
||
emit('close');
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 触发器样式 - 不可见定位点 */
|
||
.context-menu-trigger {
|
||
position: fixed;
|
||
width: 1px;
|
||
height: 1px;
|
||
pointer-events: none;
|
||
z-index: 9999;
|
||
}
|
||
|
||
/* 下拉菜单覆盖层样式 */
|
||
.context-menu-overlay {
|
||
background: white;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
padding: 4px 0;
|
||
min-width: 120px;
|
||
user-select: none;
|
||
color: #000;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* 黑色主题 */
|
||
:root[theme='dark'] .context-menu-overlay {
|
||
background: #141414;
|
||
border-color: #424242;
|
||
color: #ffffffd9;
|
||
}
|
||
|
||
/* 菜单头部 */
|
||
.context-menu-header {
|
||
background-color: #fafafa;
|
||
padding: 8px 12px;
|
||
border-radius: 6px 6px 0 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 黑色主题菜单头部 */
|
||
:root[theme='dark'] .context-menu-header {
|
||
background-color: #212121;
|
||
}
|
||
|
||
.menu-title {
|
||
color: #000 !important;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 黑色主题标题 */
|
||
:root[theme='dark'] .menu-title {
|
||
color: #ffffffd9 !important;
|
||
}
|
||
|
||
/* 关闭按钮样式 */
|
||
.close-button {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.close-button:hover {
|
||
background-color: #ff4d4f;
|
||
color: white;
|
||
}
|
||
|
||
.close-icon {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
line-height: 1;
|
||
color: #666;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.close-button:hover .close-icon {
|
||
color: white;
|
||
}
|
||
|
||
/* 黑色主题关闭按钮 */
|
||
:root[theme='dark'] .close-button {
|
||
background-color: #424242;
|
||
}
|
||
|
||
:root[theme='dark'] .close-button:hover {
|
||
background-color: #ff4d4f;
|
||
}
|
||
|
||
:root[theme='dark'] .close-icon {
|
||
color: #ffffffd9;
|
||
}
|
||
|
||
</style>
|