2025-11-06 09:20:22 +08:00

204 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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="context-menu-wrapper" @contextmenu.prevent="handleContextMenu($event)">
<!-- 插槽:包裹需要绑定右键菜单的内容 -->
<slot />
<!-- 右键菜单 -->
<div v-if="visible" class="hospital-menu" :style="{
top: `${top}px`,
left: `${left}px`,
display: visible ? 'block' : 'none',
width: `${menuWidth}px`
}" @click.stop @contextmenu.prevent>
<template v-for="(item, index) in items" :key="index">
<div v-if="item.type === 'divider'" class="menu-divider"></div>
<div v-if="item.type !== 'divider'" class="menu-item" :class="{
danger: item.danger,
disabled: item.disabled
}" @click="handleItemClick(item)">
<span class="menu-shortcut" v-if="item.shortcut">{{ item.shortcut }}</span>
<span class="menu-label">{{ item.label }}</span>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ContextMenuItem } from '@/types/index';
import emitter from '@/utils/mitt';
// 组件Props
const props = withDefaults(defineProps<{
items: ContextMenuItem[]; // 菜单项
autoClose?: boolean; // 是否自动关闭菜单
menuWidth?: number | string; // 菜单宽度
}>(), {
autoClose: true,
menuWidth: 180,
});
// 组件Emits
const emit = defineEmits<{
(e: 'select', item: ContextMenuItem): void;
(e: 'open'): void;
(e: 'close'): void;
}>();
// 菜单状态
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const handleContextMenu = async (e: MouseEvent) => {
emitter.emit('closeAllContextMenus');
// 先计算初始位置
left.value = Math.min(e.clientX, window.innerWidth - (typeof props.menuWidth === 'number' ? props.menuWidth : 180));
top.value = e.clientY;
visible.value = true;
emit('open');
// 等待菜单渲染完成后获取实际高度
await nextTick();
const menuEl = document.querySelector('.hospital-menu') as HTMLElement;
if (menuEl) {
const menuHeight = menuEl.offsetHeight;
const maxTop = window.innerHeight - menuHeight;
// 确保菜单不会超出底部可视区域
if (top.value > maxTop) {
top.value = maxTop;
// 如果向上调整后仍超出顶部,则取最小0值
if (top.value < 0) top.value = 0;
}
}
};
// 处理菜单项点击
const handleItemClick = (item: ContextMenuItem) => {
if (item.disabled) return;
emit('select', item);
if (props.autoClose !== false) {
hideMenu();
}
};
// 隐藏菜单
const hideMenu = () => {
visible.value = false;
emit('close');
};
// 点击外部区域关闭菜单
const handleClickOutside = (e: MouseEvent) => {
hideMenu();
};
// 键盘事件处理
const handleKeyDown = (e: KeyboardEvent) => {
if (!visible.value) return;
if (e.key === 'Escape') {
hideMenu();
}
// 统一转换小写
const row: ContextMenuItem | undefined = props.items.find(item => item.shortcut?.toLowerCase() === e.key?.toLowerCase());
if (row && !row.disabled) {
handleItemClick(row)
}
};
onMounted(() => {
// 关闭所有菜单
emitter.on('closeAllContextMenus', () => {
if (visible.value) {
hideMenu();
}
});
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
});
// 卸载时移除事件监听
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
emitter.off('closeAllContextMenus');
});
</script>
<style scoped>
.context-menu-wrapper {
position: relative;
height: 100%;
}
.hospital-menu {
position: fixed;
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: .25rem;
box-shadow: 0 .125rem .75rem rgba(0, 0, 0, 0.1);
z-index: 9999;
padding: .3125rem 0;
font-size: .8125rem;
font-family: SourceHanSansCN, SourceHanSansCN;
}
.menu-item {
padding: .3125rem .625rem;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color 0.2s;
/* justify-content: space-between; */
}
.menu-item:hover:not(.disabled) {
background-color: #e6f7ff;
color: #165DFF;
}
.menu-item.disabled {
color: #c0c4cc;
cursor: not-allowed;
}
.menu-item i {
margin-right: .5rem;
font-size: .875rem;
width: 1rem;
text-align: center;
}
.menu-divider {
height: 1px;
background-color: #e5e7eb;
margin: .25rem 0;
}
.menu-item.danger {
color: #f53f3f;
}
.menu-item.danger:hover:not(.disabled) {
background-color: #fff1f0;
color: #f53f3f;
}
.menu-shortcut {
color: #909399;
margin-right: .3125rem;
}
</style>