404 lines
10 KiB
Vue
404 lines
10 KiB
Vue
<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>
|