合并画布代码
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "select"]);
|
||||
|
||||
const menuRef = ref(null);
|
||||
const adjustedPosition = ref({ x: 0, y: 0 });
|
||||
const hoveredItem = ref(null);
|
||||
const submenuPositions = ref(new Map());
|
||||
const hideTimer = ref(null); // 添加隐藏定时器
|
||||
|
||||
// 计算菜单位置,处理边界问题
|
||||
const calculatePosition = () => {
|
||||
if (!menuRef.value || !props.visible) return;
|
||||
|
||||
const menu = menuRef.value;
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let x = props.position.x;
|
||||
let y = props.position.y;
|
||||
|
||||
// 右边界检测
|
||||
if (x + menuRect.width > windowWidth - 10) {
|
||||
x = x - menuRect.width;
|
||||
}
|
||||
|
||||
// 底边界检测
|
||||
if (y + menuRect.height > windowHeight - 10) {
|
||||
y = windowHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
// 左边界检测
|
||||
if (x < 10) {
|
||||
x = 10;
|
||||
}
|
||||
|
||||
// 顶边界检测
|
||||
if (y < 10) {
|
||||
y = 10;
|
||||
}
|
||||
|
||||
adjustedPosition.value = { x, y };
|
||||
};
|
||||
|
||||
// 计算子菜单位置
|
||||
const calculateSubmenuPosition = (itemElement, itemIndex) => {
|
||||
if (!itemElement || !menuRef.value) return { x: 0, y: 0, direction: "right" };
|
||||
|
||||
const itemRect = itemElement.getBoundingClientRect();
|
||||
const menuRect = menuRef.value.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// 预估子菜单宽度(可以根据实际情况调整)
|
||||
const submenuWidth = 200;
|
||||
const submenuHeight = 300; // 预估高度
|
||||
|
||||
let x = itemRect.right + 4;
|
||||
// 直接使用菜单项相对于主菜单容器的偏移量
|
||||
let y = itemElement.offsetTop;
|
||||
let direction = "right";
|
||||
|
||||
// 右边界检测,如果右侧空间不足,显示在左侧
|
||||
if (x + submenuWidth > windowWidth - 10) {
|
||||
x = itemRect.left - submenuWidth - 4;
|
||||
direction = "left";
|
||||
}
|
||||
|
||||
// 底边界检测 - 基于子菜单的绝对位置检查
|
||||
const absoluteSubmenuBottom = itemRect.top + submenuHeight;
|
||||
if (absoluteSubmenuBottom > windowHeight - 10) {
|
||||
// 计算可用的最大Y位置(相对于主菜单)
|
||||
const maxAbsoluteY = windowHeight - submenuHeight - 10;
|
||||
const maxRelativeY = maxAbsoluteY - menuRect.top;
|
||||
y = Math.max(0, maxRelativeY);
|
||||
}
|
||||
|
||||
// 左边界检测
|
||||
if (x < 10) {
|
||||
x = 10;
|
||||
direction = "right";
|
||||
}
|
||||
|
||||
// 确保 y 不为负数
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
|
||||
y = 0;
|
||||
|
||||
const position = { x, y, direction };
|
||||
submenuPositions.value.set(itemIndex, position);
|
||||
return position;
|
||||
};
|
||||
|
||||
// 清除隐藏定时器
|
||||
const clearHideTimer = () => {
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
hideTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 显示子菜单
|
||||
const showSubmenu = (item, index, element) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
clearHideTimer();
|
||||
hoveredItem.value = index;
|
||||
nextTick(() => {
|
||||
calculateSubmenuPosition(element, index);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏子菜单(延迟)
|
||||
const hideSubmenu = (index) => {
|
||||
clearHideTimer();
|
||||
hideTimer.value = setTimeout(() => {
|
||||
if (hoveredItem.value === index) {
|
||||
hoveredItem.value = null;
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// 处理鼠标进入菜单项
|
||||
const handleItemMouseEnter = (item, index, event) => {
|
||||
const element = event.target.closest(".context-menu-item");
|
||||
showSubmenu(item, index, element);
|
||||
};
|
||||
|
||||
// 处理鼠标在菜单项内移动
|
||||
const handleItemMouseMove = (item, index, event) => {
|
||||
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
|
||||
if (
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
hoveredItem.value !== index
|
||||
) {
|
||||
const element = event.target.closest(".context-menu-item");
|
||||
showSubmenu(item, index, element);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标离开菜单项
|
||||
const handleItemMouseLeave = (item, index) => {
|
||||
// 只有当有子菜单时才延迟隐藏
|
||||
if (item.children && item.children.length > 0) {
|
||||
hideSubmenu(index);
|
||||
} else {
|
||||
hoveredItem.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入子菜单
|
||||
const handleSubmenuMouseEnter = (index) => {
|
||||
clearHideTimer();
|
||||
hoveredItem.value = index;
|
||||
};
|
||||
|
||||
// 处理鼠标离开子菜单
|
||||
const handleSubmenuMouseLeave = (index) => {
|
||||
hideSubmenu(index);
|
||||
};
|
||||
|
||||
// 监听可见性和位置变化
|
||||
watch([() => props.visible, () => props.position], () => {
|
||||
if (props.visible) {
|
||||
nextTick(() => {
|
||||
calculatePosition();
|
||||
});
|
||||
} else {
|
||||
hoveredItem.value = null;
|
||||
submenuPositions.value.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理菜单项点击
|
||||
const handleItemClick = (item, index) => {
|
||||
if (item.disabled || item.type === "divider") return;
|
||||
|
||||
// 如果有子菜单,不关闭菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("select", item, index);
|
||||
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 处理子菜单项点击
|
||||
const handleSubItemClick = (subItem, parentIndex, subIndex) => {
|
||||
if (subItem.disabled || subItem.type === "divider") return;
|
||||
|
||||
emit("select", subItem, `${parentIndex}-${subIndex}`);
|
||||
|
||||
if (subItem.action) {
|
||||
subItem.action();
|
||||
}
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 处理外部点击关闭
|
||||
const handleOutsideClick = (event) => {
|
||||
if (menuRef.value && !menuRef.value.contains(event.target)) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理 ESC 键关闭
|
||||
const handleEscKey = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleOutsideClick, true);
|
||||
document.addEventListener("contextmenu", handleOutsideClick, true);
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", handleOutsideClick, true);
|
||||
document.removeEventListener("contextmenu", handleOutsideClick, true);
|
||||
document.removeEventListener("keydown", handleEscKey);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<transition name="context-menu">
|
||||
<div
|
||||
v-if="visible"
|
||||
ref="menuRef"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
top: `${adjustedPosition.y}px`,
|
||||
left: `${adjustedPosition.x}px`,
|
||||
}"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="item.type === 'divider'"
|
||||
class="context-menu-divider"
|
||||
></div>
|
||||
|
||||
<!-- 菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
class="context-menu-item"
|
||||
:class="{
|
||||
disabled: item.disabled,
|
||||
danger: item.danger,
|
||||
'has-children': item.children && item.children.length > 0,
|
||||
hovered: hoveredItem === index,
|
||||
}"
|
||||
@click="handleItemClick(item, index)"
|
||||
@mouseenter="handleItemMouseEnter(item, index, $event)"
|
||||
@mousemove="handleItemMouseMove(item, index, $event)"
|
||||
@mouseleave="handleItemMouseLeave(item, index)"
|
||||
>
|
||||
<span class="context-menu-icon" v-if="item.icon">
|
||||
<SvgIcon
|
||||
:name="item.icon"
|
||||
size="14"
|
||||
:style="{
|
||||
transform: item.inverIcon ? `rotate(90deg)` : 'none',
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
<span class="context-menu-label">{{ item.label }}</span>
|
||||
<span class="context-menu-shortcut" v-if="item.shortcut">
|
||||
{{ item.shortcut }}
|
||||
</span>
|
||||
<span
|
||||
class="context-menu-arrow"
|
||||
v-if="item.children && item.children.length > 0"
|
||||
>
|
||||
<SvgIcon name="CRight" size="12" />
|
||||
</span>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<transition name="context-submenu">
|
||||
<div
|
||||
v-if="
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
hoveredItem === index
|
||||
"
|
||||
class="context-submenu"
|
||||
:class="{
|
||||
'submenu-left':
|
||||
submenuPositions.get(index)?.direction === 'left',
|
||||
}"
|
||||
@mouseenter="handleSubmenuMouseEnter(index)"
|
||||
@mouseleave="handleSubmenuMouseLeave"
|
||||
>
|
||||
<template
|
||||
v-for="(subItem, subIndex) in item.children"
|
||||
:key="subIndex"
|
||||
>
|
||||
<!-- 子菜单分隔线 -->
|
||||
<div
|
||||
v-if="subItem.type === 'divider'"
|
||||
class="context-menu-divider"
|
||||
></div>
|
||||
|
||||
<!-- 子菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
class="context-menu-item"
|
||||
:class="{
|
||||
disabled: subItem.disabled,
|
||||
danger: subItem.danger,
|
||||
}"
|
||||
@click="handleSubItemClick(subItem, index, subIndex)"
|
||||
>
|
||||
<span class="context-menu-icon" v-if="subItem.icon">
|
||||
<SvgIcon
|
||||
:name="subItem.icon"
|
||||
size="14"
|
||||
:style="{
|
||||
transform: subItem.inverIcon
|
||||
? `rotate(90deg)`
|
||||
: 'none',
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
<span class="context-menu-label">{{ subItem.label }}</span>
|
||||
<span class="context-menu-shortcut" v-if="subItem.shortcut">
|
||||
{{ subItem.shortcut }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import "./contextMenu.less";
|
||||
</style>
|
||||
Reference in New Issue
Block a user