web-map/src/components/context-menu/storage-menu.vue

404 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="storage-menu">
<!-- 库位列表 -->
<div
v-for="location in storageLocations"
:key="location.id"
class="context-menu-item storage-item"
:class="getStorageItemClass(location)"
@click="handleSelectStorage(location)"
@mouseenter="showSubMenu = location.id"
@mouseleave="handleStorageMouseLeave"
:title="getStorageTooltip(location)"
>
<div class="storage-info">
<div class="storage-name">{{ location.name }}</div>
<div class="storage-status">
<div class="status-row">
<span class="status-indicator occupied" :class="{ active: location.isOccupied }"></span>
<span class="status-text">{{ location.isOccupied ? '已占用' : '未占用' }}</span>
</div>
<div class="status-row">
<span class="status-indicator locked" :class="{ active: location.isLocked }"></span>
<span class="status-text">{{ location.isLocked ? '已锁定' : '未锁定' }}</span>
</div>
</div>
</div>
<div class="arrow-icon"></div>
</div>
<!-- 库位操作子菜单 -->
<div
v-if="showSubMenu && selectedLocation"
class="sub-menu-container"
:style="subMenuStyle"
@click.stop
@mouseenter="handleSubMenuMouseEnter"
@mouseleave="handleSubMenuMouseLeave"
>
<!-- 连接区域确保鼠标移动时不会断开 -->
<div class="sub-menu-connector"></div>
<div class="sub-menu">
<div class="sub-menu-header">
<div class="sub-menu-title">{{ selectedLocation.name }} - 操作</div>
</div>
<div class="sub-menu-item" @click="handleStorageAction('occupy', '占用')">
<span class="action-icon">📦</span>
<span>占用</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('release', '释放')">
<span class="action-icon">📤</span>
<span>释放</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('lock', '锁定')">
<span class="action-icon">🔒</span>
<span>锁定</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('unlock', '解锁')">
<span class="action-icon">🔓</span>
<span>解锁</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('enable', '启用')">
<span class="action-icon"></span>
<span>启用</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('disable', '禁用')">
<span class="action-icon"></span>
<span>禁用</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('set_empty_tray', '设为空托盘')">
<span class="action-icon">📋</span>
<span>设为空托盘</span>
</div>
<div class="sub-menu-item" @click="handleStorageAction('clear_empty_tray', '清除空托盘')">
<span class="action-icon">🗑</span>
<span>清除空托盘</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import type { StorageLocationInfo } from '../../services/context-menu';
interface Props {
storageLocations: StorageLocationInfo[];
menuX: number;
menuY: number;
mainMenuWidth: number;
}
interface Emits {
(e: 'actionComplete', data: { action: string; location: StorageLocationInfo; success: boolean }): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// 定义组件名称
defineOptions({
name: 'StorageMenu',
});
// 子菜单状态
const showSubMenu = ref<string | null>(null);
const selectedLocation = ref<StorageLocationInfo | null>(null);
const hideTimer = ref<number | null>(null);
// 监听子菜单显示状态
watch(showSubMenu, (newValue) => {
if (newValue) {
selectedLocation.value = props.storageLocations.find(loc => loc.id === newValue) || null;
// 清除之前的隐藏定时器
if (hideTimer.value) {
clearTimeout(hideTimer.value);
hideTimer.value = null;
}
} else {
selectedLocation.value = null;
}
});
// 子菜单样式计算
const subMenuStyle = computed(() => {
if (!showSubMenu.value) return {};
// 计算子菜单位置,让它与对应的库位项对齐
const menuItemHeight = 60; // 库位项的高度
const headerHeight = 40; // 菜单头部高度
const itemIndex = props.storageLocations.findIndex(loc => loc.id === showSubMenu.value);
const offsetY = headerHeight + (itemIndex * menuItemHeight);
const style = {
left: `${props.menuX + props.mainMenuWidth}px`, // 紧贴主菜单右边缘
top: `${props.menuY + offsetY}px`, // 与对应库位项对齐
};
return style;
});
// 选择库位
const handleSelectStorage = (location: StorageLocationInfo) => {
console.log('选择库位:', location);
// 不立即关闭菜单,让子菜单显示
};
// 隐藏子菜单(立即隐藏)
const hideSubMenu = () => {
showSubMenu.value = null;
if (hideTimer.value) {
clearTimeout(hideTimer.value);
hideTimer.value = null;
}
};
// 延迟隐藏子菜单
const hideSubMenuDelayed = () => {
if (hideTimer.value) {
clearTimeout(hideTimer.value);
}
hideTimer.value = window.setTimeout(() => {
showSubMenu.value = null;
hideTimer.value = null;
}, 150); // 150ms延迟给用户足够时间移动到子菜单
};
// 处理库位项鼠标离开
const handleStorageMouseLeave = () => {
hideSubMenuDelayed();
};
// 处理子菜单鼠标进入
const handleSubMenuMouseEnter = () => {
// 清除隐藏定时器
if (hideTimer.value) {
clearTimeout(hideTimer.value);
hideTimer.value = null;
}
};
// 处理子菜单鼠标离开
const handleSubMenuMouseLeave = () => {
hideSubMenu();
};
// 库位操作处理
const handleStorageAction = async (action: string, actionName: string) => {
if (!selectedLocation.value) return;
try {
// 直接调用API不需要勾选
const { StorageActionService } = await import('../../services/storageActionService');
await StorageActionService.handleStorageAction(action, selectedLocation.value, actionName);
// 发送操作完成事件
emit('actionComplete', {
action,
location: selectedLocation.value,
success: true
});
} catch (error) {
console.error(`库位${actionName}操作失败:`, error);
// 发送操作失败事件
emit('actionComplete', {
action,
location: selectedLocation.value,
success: false
});
}
};
// 获取库位项样式类
const getStorageItemClass = (location: StorageLocationInfo) => {
return {
'storage-occupied': location.isOccupied,
'storage-locked': location.isLocked,
'storage-available': !location.isOccupied && !location.isLocked,
};
};
// 获取库位提示信息
const getStorageTooltip = (location: StorageLocationInfo) => {
const occupiedText = location.isOccupied ? '已占用' : '未占用';
const lockedText = location.isLocked ? '已锁定' : '未锁定';
return `${location.name} - 占用: ${occupiedText}, 锁定: ${lockedText}`;
};
</script>
<style scoped>
.storage-menu {
width: 100%;
}
/* 库位项样式 */
.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;
flex-direction: column;
gap: 2px;
font-size: 12px;
}
.status-row {
display: flex;
align-items: center;
gap: 6px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
background-color: #d9d9d9; /* 默认灰色 */
transition: background-color 0.2s;
}
.status-indicator.occupied {
background-color: #d9d9d9; /* 默认灰色 */
}
.status-indicator.occupied.active {
background-color: #ff4d4f; /* 占用时红色 */
}
.status-indicator.locked {
background-color: #d9d9d9; /* 默认灰色 */
}
.status-indicator.locked.active {
background-color: #faad14; /* 锁定时橙色 */
}
.status-text {
color: #666;
font-size: 11px;
}
/* 库位状态样式 */
.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;
}
/* 箭头图标 */
.arrow-icon {
margin-left: auto;
color: #999;
font-size: 12px;
transition: color 0.2s;
}
.storage-item:hover .arrow-icon {
color: #1890ff;
}
/* 子菜单容器样式 */
.sub-menu-container {
position: fixed;
z-index: 2;
pointer-events: auto;
display: flex;
align-items: flex-start;
}
/* 连接区域 - 确保鼠标移动流畅 */
.sub-menu-connector {
width: 12px;
height: 100%;
background: transparent;
pointer-events: auto;
/* 确保连接区域覆盖可能的间隙 */
margin-left: -6px;
/* 添加一个微妙的背景,帮助用户理解连接关系 */
background: linear-gradient(to right, transparent 0%, rgba(24, 144, 255, 0.05) 50%, transparent 100%);
}
/* 子菜单样式 */
.sub-menu {
background: white;
border: 1px solid #d9d9d9;
border-left: none; /* 移除左边框,与主菜单无缝连接 */
border-radius: 0 6px 6px 0; /* 只圆角右侧,左侧与主菜单连接 */
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.15); /* 调整阴影,避免左侧阴影 */
padding: 4px 0;
min-width: 160px;
user-select: none;
color: #000;
pointer-events: auto;
/* 确保与主菜单无缝连接 */
margin-left: 0;
/* 添加一个微妙的左边框,模拟与主菜单的连接 */
border-left: 1px solid #e8e8e8;
}
.sub-menu-header {
padding: 8px 12px;
background-color: #fafafa;
border-radius: 0 6px 0 0; /* 只圆角右上角,与子菜单整体设计一致 */
border-bottom: 1px solid #f0f0f0;
}
.sub-menu-title {
font-size: 13px;
font-weight: 600;
color: #000;
}
.sub-menu-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 13px;
color: #000;
gap: 8px;
}
.sub-menu-item:hover {
background-color: #f5f5f5;
}
.action-icon {
font-size: 14px;
width: 16px;
text-align: center;
}
</style>