feat: 添加右键菜单管理功能,支持自定义事件处理和全局点击/键盘事件监听,添加库位右键菜单渲染
This commit is contained in:
parent
14278291c6
commit
e39da1cde5
263
src/components/context-menu.vue
Normal file
263
src/components/context-menu.vue
Normal file
@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div v-if="visible" class="context-menu" :style="menuStyle" @click.stop @contextmenu.prevent>
|
||||
<div class="context-menu-header">
|
||||
<div class="menu-title">{{ headerTitle }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 库位列表菜单 -->
|
||||
<template v-if="(menuType === 'storage' || menuType === 'storage-background') && storageLocations.length">
|
||||
<div
|
||||
v-for="location in storageLocations"
|
||||
:key="location.id"
|
||||
class="context-menu-item storage-item"
|
||||
:class="getStorageItemClass(location)"
|
||||
@click="handleSelectStorage(location)"
|
||||
:title="getStorageTooltip(location)"
|
||||
>
|
||||
<div class="storage-info">
|
||||
<div class="storage-name">{{ location.name }}</div>
|
||||
<div class="storage-status">
|
||||
<span class="status-indicator" :class="location.status"></span>
|
||||
<span class="status-text">{{ getStatusText(location) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 默认菜单:仅刷新 -->
|
||||
<template v-else>
|
||||
<div class="context-menu-item" @click="handleRefresh">
|
||||
<span>刷新</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface StorageLocationInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
isOccupied: boolean;
|
||||
isLocked: boolean;
|
||||
status: 'available' | 'occupied' | 'locked' | 'unknown';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
x?: number;
|
||||
y?: number;
|
||||
menuType?: 'default' | 'storage' | 'storage-background';
|
||||
storageLocations?: StorageLocationInfo[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
x: 0,
|
||||
y: 0,
|
||||
menuType: 'default',
|
||||
storageLocations: () => [],
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'ContextMenu',
|
||||
});
|
||||
|
||||
// 菜单样式计算
|
||||
const menuStyle = computed(() => {
|
||||
const style = {
|
||||
left: `${props.x}px`,
|
||||
top: `${props.y}px`,
|
||||
};
|
||||
return style;
|
||||
});
|
||||
|
||||
// 动态标题
|
||||
const headerTitle = computed(() => {
|
||||
if (props.menuType === 'storage-background') return '库位状态';
|
||||
if (props.menuType === 'storage') return '库位信息';
|
||||
return '右键菜单';
|
||||
});
|
||||
|
||||
// 处理刷新点击
|
||||
const handleRefresh = () => {
|
||||
console.log('刷新操作');
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// 选择库位
|
||||
const handleSelectStorage = (location: StorageLocationInfo) => {
|
||||
console.log('选择库位:', location);
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// 获取库位项样式类
|
||||
const getStorageItemClass = (location: StorageLocationInfo) => {
|
||||
return {
|
||||
'storage-occupied': location.isOccupied,
|
||||
'storage-locked': location.isLocked,
|
||||
'storage-available': !location.isOccupied && !location.isLocked,
|
||||
};
|
||||
};
|
||||
|
||||
// 获取库位提示信息
|
||||
const getStorageTooltip = (location: StorageLocationInfo) => {
|
||||
const status = getStatusText(location);
|
||||
return `${location.name} - ${status}`;
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (location: StorageLocationInfo) => {
|
||||
switch (location.status) {
|
||||
case 'occupied':
|
||||
return '已占用';
|
||||
case 'locked':
|
||||
return '已锁定';
|
||||
case 'available':
|
||||
return '可用';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.context-menu-item:hover .menu-icon {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 菜单头部 */
|
||||
.context-menu-header {
|
||||
padding: 8px 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
background-color: #d9d9d9;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* 库位项样式 */
|
||||
.storage-item {
|
||||
padding: 12px;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.storage-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-left-color: #1890ff;
|
||||
}
|
||||
|
||||
.storage-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.storage-name {
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.storage-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-indicator.available {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.status-indicator.occupied {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-indicator.locked {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
.status-indicator.unknown {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 库位状态样式 */
|
||||
.storage-occupied {
|
||||
background-color: #fff2f0;
|
||||
border-left-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.storage-locked {
|
||||
background-color: #fffbe6;
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.storage-available {
|
||||
background-color: #f6ffed;
|
||||
border-left-color: #52c41a;
|
||||
}
|
||||
</style>
|
@ -9,8 +9,12 @@ import { isNil } from 'lodash-es';
|
||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import MapToolbar from '../components/map-toolbar.vue';
|
||||
import { autoDoorSimulationService, type AutoDoorWebSocketData } from '../services/auto-door-simulation.service';
|
||||
import {
|
||||
type ContextMenuState,
|
||||
createContextMenuManager,
|
||||
handleContextMenuFromPenData,
|
||||
isClickInsideMenu} from '../services/context-menu.service';
|
||||
|
||||
const EDITOR_KEY = Symbol('editor-key');
|
||||
|
||||
@ -237,6 +241,22 @@ onMounted(async () => {
|
||||
// enableLogging: true,
|
||||
// });
|
||||
}
|
||||
|
||||
// 订阅右键菜单状态变化
|
||||
contextMenuManager.subscribe((state) => {
|
||||
contextMenuState.value = state;
|
||||
});
|
||||
|
||||
// 监听EditorService的自定义右键菜单事件
|
||||
if (editor.value) {
|
||||
(editor.value as any).on('customContextMenu', (event: Record<string, unknown>) => {
|
||||
handleEditorContextMenu(event);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加全局点击事件监听器,用于关闭右键菜单
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
document.addEventListener('keydown', handleGlobalKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -246,6 +266,17 @@ onUnmounted(() => {
|
||||
autoDoorSimulationService.clearBufferedData();
|
||||
// 注释掉模拟相关的清理,只保留WebSocket数据处理的清理
|
||||
// autoDoorSimulationService.stopAllSimulations();
|
||||
|
||||
// 移除EditorService事件监听器
|
||||
if (editor.value) {
|
||||
(editor.value as any).off('customContextMenu', (event: Record<string, unknown>) => {
|
||||
handleEditorContextMenu(event);
|
||||
});
|
||||
}
|
||||
|
||||
// 移除全局事件监听器
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
document.removeEventListener('keydown', handleGlobalKeydown);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
@ -313,12 +344,69 @@ const handleAutoSaveAndRestoreViewState = async () => {
|
||||
|
||||
//#region UI状态管理
|
||||
const show = ref<boolean>(true);
|
||||
|
||||
// 右键菜单状态管理 - 使用组合式函数
|
||||
const contextMenuManager = createContextMenuManager();
|
||||
const contextMenuState = ref<ContextMenuState>(contextMenuManager.getState());
|
||||
//#endregion
|
||||
|
||||
// 返回到父级 iframe 的场景卡片
|
||||
const backToCards = () => {
|
||||
window.parent?.postMessage({ type: 'scene_return_to_cards' }, '*');
|
||||
};
|
||||
|
||||
//#region 右键菜单处理
|
||||
/**
|
||||
* 处理EditorService的自定义右键菜单事件
|
||||
* @param penData EditorService传递的pen数据
|
||||
*/
|
||||
const handleEditorContextMenu = (penData: Record<string, unknown>) => {
|
||||
console.log('EditorService自定义右键菜单事件:', penData);
|
||||
handleContextMenuFromPenData(penData, contextMenuManager, storageLocationService.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理原生右键菜单事件(作为备用)
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
const handleContextMenuEvent = (event: MouseEvent) => {
|
||||
// 这个函数现在作为备用,因为主要逻辑在handleEditorContextMenu中
|
||||
console.log('原生右键菜单事件(备用):', event);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 关闭右键菜单
|
||||
*/
|
||||
const handleCloseContextMenu = () => {
|
||||
contextMenuManager.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理全局点击事件,用于关闭右键菜单
|
||||
* @param event 点击事件
|
||||
*/
|
||||
const handleGlobalClick = (event: MouseEvent) => {
|
||||
// 如果右键菜单可见,检查点击是否在菜单内
|
||||
if (contextMenuState.value.visible) {
|
||||
// 如果点击不在菜单内,则关闭菜单
|
||||
if (!isClickInsideMenu(event, contextMenuState.value.visible)) {
|
||||
contextMenuManager.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理全局键盘事件,ESC键关闭右键菜单
|
||||
* @param event 键盘事件
|
||||
*/
|
||||
const handleGlobalKeydown = (event: KeyboardEvent) => {
|
||||
// ESC键关闭右键菜单
|
||||
if (event.key === 'Escape' && contextMenuState.value.visible) {
|
||||
contextMenuManager.close();
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -348,7 +436,7 @@ const backToCards = () => {
|
||||
</a-tabs>
|
||||
</a-layout-sider>
|
||||
<a-layout-content>
|
||||
<div ref="container" class="editor-container full"></div>
|
||||
<div ref="container" class="editor-container full" @contextmenu="handleContextMenuEvent"></div>
|
||||
<!-- 自定义地图工具栏(固定右下角,最小侵入) -->
|
||||
<MapToolbar :token="EDITOR_KEY" :container-el="container" />
|
||||
</a-layout-content>
|
||||
@ -371,11 +459,23 @@ const backToCards = () => {
|
||||
<AreaDetailCard v-if="isArea" :token="EDITOR_KEY" :current="current.id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ContextMenu
|
||||
:visible="contextMenuState.visible"
|
||||
:x="contextMenuState.x"
|
||||
:y="contextMenuState.y"
|
||||
:menu-type="contextMenuState.menuType"
|
||||
:storage-locations="contextMenuState.storageLocations"
|
||||
@close="handleCloseContextMenu"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-container {
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
|
635
src/services/context-menu.service.ts
Normal file
635
src/services/context-menu.service.ts
Normal file
@ -0,0 +1,635 @@
|
||||
/**
|
||||
* 右键菜单服务 - 组合式函数模式
|
||||
* 提供纯函数和状态管理工具,避免单例模式的问题
|
||||
*/
|
||||
|
||||
export interface ParsedEventData {
|
||||
type: 'robot' | 'point' | 'area' | 'storage' | 'storage-background' | 'default';
|
||||
id?: string;
|
||||
name?: string;
|
||||
pointId?: string; // 关联的点ID
|
||||
storageId?: string; // 库位ID
|
||||
tags?: string[]; // 标签信息
|
||||
target: HTMLElement;
|
||||
position: { x: number; y: number };
|
||||
pen?: Record<string, unknown>; // 原始pen对象数据
|
||||
}
|
||||
|
||||
export interface StorageLocationInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
isOccupied: boolean; // 是否占用
|
||||
isLocked: boolean; // 是否锁定
|
||||
status: 'available' | 'occupied' | 'locked' | 'unknown';
|
||||
}
|
||||
|
||||
export interface ContextMenuState {
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
menuType: 'default' | 'storage' | 'storage-background';
|
||||
storageLocations: StorageLocationInfo[];
|
||||
eventData?: ParsedEventData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 右键菜单状态管理器
|
||||
* 使用组合式函数,避免单例模式
|
||||
*/
|
||||
export function createContextMenuManager() {
|
||||
let state: ContextMenuState = {
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
menuType: 'default',
|
||||
storageLocations: [],
|
||||
};
|
||||
|
||||
const listeners: Array<(state: ContextMenuState) => void> = [];
|
||||
|
||||
/**
|
||||
* 订阅状态变化
|
||||
*/
|
||||
function subscribe(listener: (state: ContextMenuState) => void) {
|
||||
listeners.push(listener);
|
||||
return () => {
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知状态变化
|
||||
*/
|
||||
function notify() {
|
||||
listeners.forEach(listener => listener({ ...state }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
function getState(): ContextMenuState {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*/
|
||||
function setState(newState: Partial<ContextMenuState>) {
|
||||
state = { ...state, ...newState };
|
||||
notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭菜单
|
||||
*/
|
||||
function close() {
|
||||
setState({ visible: false });
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
getState,
|
||||
setState,
|
||||
close,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析penData - 纯函数,无副作用
|
||||
* @param penData EditorService传递的pen数据
|
||||
* @returns 解析后的事件数据
|
||||
*/
|
||||
export function parsePenData(penData: Record<string, unknown>): ParsedEventData {
|
||||
// 从penData中提取pen数据和事件信息
|
||||
const pen = penData.pen as Record<string, unknown>;
|
||||
const eventInfo = penData.e as { clientX: number; clientY: number };
|
||||
|
||||
const position = {
|
||||
x: eventInfo?.clientX || 0,
|
||||
y: eventInfo?.clientY || 0
|
||||
};
|
||||
|
||||
// 如果有pen数据,优先使用pen信息进行解析
|
||||
if (pen) {
|
||||
console.log('解析pen数据:', pen);
|
||||
const { id, name, tags = [], storageLocation } = pen as {
|
||||
id?: string;
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
storageLocation?: {
|
||||
pointId: string;
|
||||
locationName: string;
|
||||
index: number;
|
||||
occupied: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
console.log('解析后的数据:', { id, name, tags, storageLocation });
|
||||
|
||||
// 根据tags判断类型
|
||||
if (tags.includes('storage-background')) {
|
||||
console.log('识别为storage-background类型');
|
||||
// 库位背景区域 - 需要查找该点关联的所有库位
|
||||
const pointId = tags.find((tag: string) => tag.startsWith('point-'))?.replace('point-', '');
|
||||
return {
|
||||
type: 'storage-background',
|
||||
id,
|
||||
name,
|
||||
pointId,
|
||||
storageId: id,
|
||||
tags,
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('storage-location')) {
|
||||
console.log('识别为storage-location类型');
|
||||
// 库位区域 - 使用storageLocation中的详细信息
|
||||
const pointId = tags.find((tag: string) => tag.startsWith('point-'))?.replace('point-', '');
|
||||
return {
|
||||
type: 'storage',
|
||||
id,
|
||||
name: storageLocation?.locationName || name,
|
||||
pointId,
|
||||
storageId: id,
|
||||
tags,
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('point')) {
|
||||
// 点区域
|
||||
return {
|
||||
type: 'point',
|
||||
id,
|
||||
name,
|
||||
pointId: id,
|
||||
tags,
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('robot')) {
|
||||
// 机器人区域
|
||||
return {
|
||||
type: 'robot',
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('area')) {
|
||||
// 区域
|
||||
return {
|
||||
type: 'area',
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 默认情况
|
||||
console.log('未识别到特定类型,使用默认类型');
|
||||
return {
|
||||
type: 'default',
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析事件数据 - 纯函数,无副作用
|
||||
* @param event 鼠标事件或指针事件
|
||||
* @returns 解析后的事件数据
|
||||
*/
|
||||
export function parseEventData(event: MouseEvent | PointerEvent): ParsedEventData {
|
||||
const target = event.target as HTMLElement;
|
||||
const position = { x: event.clientX, y: event.clientY };
|
||||
// 从事件对象中获取pen数据(如果存在)
|
||||
const pen = (event as MouseEvent & { pen?: Record<string, unknown> }).pen;
|
||||
|
||||
// 如果有pen数据,优先使用pen信息进行解析
|
||||
if (pen) {
|
||||
console.log('解析pen数据:', pen);
|
||||
const { id, name, tags = [], storageLocation } = pen as {
|
||||
id?: string;
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
storageLocation?: {
|
||||
pointId: string;
|
||||
locationName: string;
|
||||
index: number;
|
||||
occupied: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
console.log('解析后的数据:', { id, name, tags, storageLocation });
|
||||
// 根据tags判断类型
|
||||
if (tags.includes('storage-background')) {
|
||||
console.log('识别为storage-background类型');
|
||||
// 库位背景区域
|
||||
const pointId = tags.find((tag: string) => tag.startsWith('point-'))?.replace('point-', '');
|
||||
return {
|
||||
type: 'storage-background',
|
||||
id,
|
||||
name,
|
||||
pointId,
|
||||
storageId: id,
|
||||
tags,
|
||||
target,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('storage-location')) {
|
||||
console.log('识别为storage-location类型');
|
||||
// 库位区域 - 使用storageLocation中的详细信息
|
||||
const pointId = tags.find((tag: string) => tag.startsWith('point-'))?.replace('point-', '');
|
||||
return {
|
||||
type: 'storage',
|
||||
id,
|
||||
name: storageLocation?.locationName || name,
|
||||
pointId,
|
||||
storageId: id,
|
||||
tags,
|
||||
target,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('point')) {
|
||||
// 点区域
|
||||
return {
|
||||
type: 'point',
|
||||
id,
|
||||
name,
|
||||
pointId: id,
|
||||
tags,
|
||||
target,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('robot')) {
|
||||
// 机器人区域
|
||||
return {
|
||||
type: 'robot',
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
target,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
|
||||
if (tags.includes('area')) {
|
||||
// 区域
|
||||
return {
|
||||
type: 'area',
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
target,
|
||||
position,
|
||||
pen,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到DOM元素检查
|
||||
if (target?.closest('.robot-item')) {
|
||||
return {
|
||||
type: 'robot',
|
||||
id: target.dataset.robotId || target.id,
|
||||
name: target.dataset.robotName || target.textContent?.trim(),
|
||||
target,
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
if (target?.closest('.point-item')) {
|
||||
return {
|
||||
type: 'point',
|
||||
id: target.dataset.pointId || target.id,
|
||||
name: target.dataset.pointName || target.textContent?.trim(),
|
||||
target,
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
if (target?.closest('.area-item')) {
|
||||
return {
|
||||
type: 'area',
|
||||
id: target.dataset.areaId || target.id,
|
||||
name: target.dataset.areaName || target.textContent?.trim(),
|
||||
target,
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
if (target?.closest('.storage-location')) {
|
||||
return {
|
||||
type: 'storage',
|
||||
id: target.dataset.storageId || target.id,
|
||||
name: target.dataset.storageName || target.textContent?.trim(),
|
||||
target,
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
// 默认情况
|
||||
console.log('未识别到特定类型,使用默认类型');
|
||||
return {
|
||||
type: 'default',
|
||||
target,
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查点击是否在菜单区域内 - 纯函数
|
||||
* @param event 点击事件
|
||||
* @param isMenuVisible 菜单是否可见
|
||||
* @returns 是否在菜单区域内
|
||||
*/
|
||||
export function isClickInsideMenu(event: MouseEvent, isMenuVisible: boolean): boolean {
|
||||
if (!isMenuVisible) return false;
|
||||
|
||||
const menuElement = document.querySelector('.context-menu');
|
||||
if (!menuElement) return false;
|
||||
|
||||
const rect = menuElement.getBoundingClientRect();
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
|
||||
return (
|
||||
x >= rect.left &&
|
||||
x <= rect.right &&
|
||||
y >= rect.top &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单配置 - 纯函数
|
||||
* @param type 事件类型
|
||||
* @param data 事件数据
|
||||
* @returns 菜单配置
|
||||
*/
|
||||
export function getMenuConfig(type: string, data: ParsedEventData, storageLocationService?: any) {
|
||||
switch (type) {
|
||||
case 'storage-background': {
|
||||
// 库位背景区域:显示该点关联的所有库位信息
|
||||
console.log(`处理storage-background类型,点ID: ${data.pointId}`);
|
||||
const allStorageLocations = getStorageLocationsForPoint(data, storageLocationService);
|
||||
console.log(`找到 ${allStorageLocations.length} 个库位:`, allStorageLocations);
|
||||
return {
|
||||
menuType: 'storage-background' as const,
|
||||
storageLocations: allStorageLocations,
|
||||
};
|
||||
}
|
||||
case 'storage':
|
||||
// 单个库位区域
|
||||
return {
|
||||
menuType: 'storage' as const,
|
||||
storageLocations: [getStorageLocationInfo(data)],
|
||||
};
|
||||
case 'point':
|
||||
// 点区域:显示该点关联的库位信息
|
||||
return {
|
||||
menuType: 'storage' as const,
|
||||
storageLocations: getStorageLocationsForPoint(data, storageLocationService),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
menuType: 'default' as const,
|
||||
storageLocations: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个库位信息
|
||||
* @param data 事件数据
|
||||
* @returns 库位信息
|
||||
*/
|
||||
function getStorageLocationInfo(data: ParsedEventData): StorageLocationInfo {
|
||||
const pen = data.pen as Record<string, unknown>;
|
||||
|
||||
// 优先使用storageLocation中的数据
|
||||
if (pen?.storageLocation) {
|
||||
const storageLocation = pen.storageLocation as {
|
||||
locationName: string;
|
||||
occupied: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
const { locationName, occupied, locked } = storageLocation;
|
||||
|
||||
let status: StorageLocationInfo['status'] = 'unknown';
|
||||
if (locked) {
|
||||
status = 'locked';
|
||||
} else if (occupied) {
|
||||
status = 'occupied';
|
||||
} else {
|
||||
status = 'available';
|
||||
}
|
||||
|
||||
return {
|
||||
id: data.id || data.storageId || 'unknown',
|
||||
name: locationName || data.name || '未知库位',
|
||||
isOccupied: occupied,
|
||||
isLocked: locked,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
// 回退到pen的其他属性
|
||||
const isLocked = pen?.locked === 1;
|
||||
const isOccupied = pen?.occupied === 1 || pen?.status === 'occupied';
|
||||
|
||||
let status: StorageLocationInfo['status'] = 'unknown';
|
||||
if (isLocked) {
|
||||
status = 'locked';
|
||||
} else if (isOccupied) {
|
||||
status = 'occupied';
|
||||
} else {
|
||||
status = 'available';
|
||||
}
|
||||
|
||||
return {
|
||||
id: data.id || data.storageId || 'unknown',
|
||||
name: data.name || '未知库位',
|
||||
isOccupied,
|
||||
isLocked,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定点ID的所有库位信息
|
||||
* @param pointId 点ID
|
||||
* @param storageLocationService StorageLocationService实例(可选)
|
||||
* @returns 库位信息列表
|
||||
*/
|
||||
function findStorageLocationsByPointId(pointId: string, storageLocationService?: any): StorageLocationInfo[] {
|
||||
console.log(`查找点 ${pointId} 关联的所有库位`);
|
||||
|
||||
// 如果提供了StorageLocationService,尝试使用它获取真实数据
|
||||
if (storageLocationService && typeof storageLocationService.getLocationsByPointId === 'function') {
|
||||
const locations = storageLocationService.getLocationsByPointId(pointId);
|
||||
if (locations && locations.length > 0) {
|
||||
console.log(`从StorageLocationService获取到 ${locations.length} 个库位:`, locations);
|
||||
// 转换StorageLocationService的数据格式到我们的格式
|
||||
const convertedLocations = locations.map((location: any) => {
|
||||
const converted = {
|
||||
id: location.id || `storage-${pointId}-${location.layer_index || 0}`,
|
||||
name: location.layer_name || location.locationName || location.name || '未知库位',
|
||||
isOccupied: location.is_occupied || location.occupied || false,
|
||||
isLocked: location.is_locked || location.locked || false,
|
||||
status: (location.is_locked || location.locked)
|
||||
? 'locked'
|
||||
: (location.is_occupied || location.occupied)
|
||||
? 'occupied'
|
||||
: 'available',
|
||||
};
|
||||
console.log(`转换库位数据: ${location.layer_name} -> ${converted.name}, 占用: ${converted.isOccupied}, 锁定: ${converted.isLocked}, 状态: ${converted.status}`);
|
||||
return converted;
|
||||
});
|
||||
return convertedLocations;
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到模拟数据
|
||||
console.log('使用模拟数据');
|
||||
const mockStorageLocations: StorageLocationInfo[] = [
|
||||
{
|
||||
id: `storage-${pointId}-0`,
|
||||
name: 'GSA-1-1-1', // 模拟库位名称
|
||||
isOccupied: false,
|
||||
isLocked: false,
|
||||
status: 'available',
|
||||
},
|
||||
{
|
||||
id: `storage-${pointId}-1`,
|
||||
name: 'GSA-1-1-2',
|
||||
isOccupied: true,
|
||||
isLocked: false,
|
||||
status: 'occupied',
|
||||
},
|
||||
{
|
||||
id: `storage-${pointId}-2`,
|
||||
name: 'GSA-1-1-3',
|
||||
isOccupied: false,
|
||||
isLocked: true,
|
||||
status: 'locked',
|
||||
},
|
||||
];
|
||||
|
||||
return mockStorageLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取点关联的库位信息列表
|
||||
* @param data 事件数据
|
||||
* @param storageLocationService StorageLocationService实例(可选)
|
||||
* @returns 库位信息列表
|
||||
*/
|
||||
function getStorageLocationsForPoint(data: ParsedEventData, storageLocationService?: any): StorageLocationInfo[] {
|
||||
const pointId = data.pointId || data.id;
|
||||
|
||||
if (!pointId) {
|
||||
console.warn('无法获取点ID,返回空库位列表');
|
||||
return [];
|
||||
}
|
||||
|
||||
return findStorageLocationsByPointId(pointId, storageLocationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理右键事件 - 组合函数
|
||||
* @param event 鼠标事件或指针事件
|
||||
* @param manager 状态管理器
|
||||
*/
|
||||
export function handleContextMenu(
|
||||
event: MouseEvent | PointerEvent,
|
||||
manager: ReturnType<typeof createContextMenuManager>
|
||||
) {
|
||||
// 阻止默认右键菜单
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 1. 解析事件数据
|
||||
const parsedData = parseEventData(event);
|
||||
|
||||
// 2. 获取菜单配置
|
||||
const menuConfig = getMenuConfig(parsedData.type, parsedData);
|
||||
|
||||
// 3. 更新状态
|
||||
manager.setState({
|
||||
visible: true,
|
||||
x: parsedData.position.x,
|
||||
y: parsedData.position.y,
|
||||
menuType: menuConfig.menuType,
|
||||
storageLocations: menuConfig.storageLocations,
|
||||
eventData: parsedData,
|
||||
});
|
||||
|
||||
console.log('右键菜单事件数据:', parsedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理EditorService的penData - 组合函数
|
||||
* @param penData EditorService传递的pen数据
|
||||
* @param manager 状态管理器
|
||||
* @param storageLocationService StorageLocationService实例(可选)
|
||||
*/
|
||||
export function handleContextMenuFromPenData(
|
||||
penData: Record<string, unknown>,
|
||||
manager: ReturnType<typeof createContextMenuManager>,
|
||||
storageLocationService?: any
|
||||
) {
|
||||
// 1. 解析penData
|
||||
const parsedData = parsePenData(penData);
|
||||
|
||||
// 2. 获取菜单配置
|
||||
const menuConfig = getMenuConfig(parsedData.type, parsedData, storageLocationService);
|
||||
|
||||
// 3. 更新状态
|
||||
manager.setState({
|
||||
visible: true,
|
||||
x: parsedData.position.x,
|
||||
y: parsedData.position.y,
|
||||
menuType: menuConfig.menuType,
|
||||
storageLocations: menuConfig.storageLocations,
|
||||
eventData: parsedData,
|
||||
});
|
||||
|
||||
console.log('右键菜单事件数据:', parsedData);
|
||||
}
|
||||
|
||||
// 为了向后兼容,提供一个默认的状态管理器实例
|
||||
// 但建议在组件中创建独立的状态管理器
|
||||
export const defaultContextMenuManager = createContextMenuManager();
|
Loading…
x
Reference in New Issue
Block a user