Files
xk-admin/apps/web-antd/src/components/context-menu/global-context-menu.vue
李琦 69862cd982
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
1. Excel交互组件封装
2026-06-25 16:09:07 +08:00

190 lines
4.3 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.
<script lang="ts" setup>
import { nextTick } from 'vue';
import type { ContextMenuItem } from './types';
import MIcon from '#/components/icon/icon.vue';
import {
contextMenuState,
getSubmenuPosition,
hideContextMenu,
MENU_WIDTH,
showSubmenu,
submenuState,
} from './use-context-menu';
defineOptions({ name: 'GlobalContextMenu' });
function onItemEnter(item: ContextMenuItem, e: MouseEvent) {
if (item.type === 'divider') {
showSubmenu('', [], 0, 0);
return;
}
if (!item.children?.length || item.disabled) {
showSubmenu('', [], 0, 0);
return;
}
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
const pos = getSubmenuPosition(rect, item.children.length);
showSubmenu(item.key, item.children, pos.left, pos.top);
}
function handleClick(item: ContextMenuItem) {
if (item.disabled || item.children?.length) {
return;
}
const payload = contextMenuState.payload;
hideContextMenu();
void nextTick(() => {
item.handler?.(payload);
});
}
function handleSubmenuClick(item: ContextMenuItem) {
if (item.disabled) {
return;
}
const payload = contextMenuState.payload;
hideContextMenu();
void nextTick(() => {
item.handler?.(payload);
});
}
</script>
<template>
<Teleport to="body">
<div
v-if="contextMenuState.visible"
class="global-context-menu"
:style="{ left: `${contextMenuState.x}px`, top: `${contextMenuState.y}px`, width: `${MENU_WIDTH}px` }"
@contextmenu.prevent
>
<template v-for="item in contextMenuState.items" :key="item.key">
<div v-if="item.type === 'divider'" class="context-menu-divider" />
<div
v-else
class="context-menu-item"
:class="{ disabled: item.disabled, 'has-children': !!item.children?.length }"
@click.stop="handleClick(item)"
@mouseenter="onItemEnter(item, $event)"
>
<span class="item-icon">
<component :is="item.icon" v-if="item.icon" />
<MIcon v-else-if="item.iconName" :icon="item.iconName" size="14" />
</span>
<span class="item-label">{{ item.label }}</span>
<span v-if="item.children?.length" class="item-arrow"></span>
</div>
</template>
</div>
<div
v-if="contextMenuState.visible && submenuState.activeKey && submenuState.items.length"
class="global-context-menu submenu"
:style="{ left: `${submenuState.left}px`, top: `${submenuState.top}px`, width: `${MENU_WIDTH}px` }"
@contextmenu.prevent
>
<div
v-for="child in submenuState.items"
:key="child.key"
class="context-menu-item"
:class="{ disabled: child.disabled }"
@click.stop="handleSubmenuClick(child)"
>
<span class="item-icon">
<component :is="child.icon" v-if="child.icon" />
<MIcon v-else-if="child.iconName" :icon="child.iconName" size="14" />
</span>
<span class="item-label">{{ child.label }}</span>
</div>
</div>
</Teleport>
</template>
<style scoped lang="scss">
.global-context-menu {
position: fixed;
z-index: 9999;
padding: 4px 0;
background: #fff;
border: 1px solid #e5e6eb;
border-radius: 8px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.context-menu-item {
display: flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 0 12px;
font-size: 13px;
color: #1d2129;
cursor: pointer;
user-select: none;
&:hover:not(.disabled) {
background: #f2f7ff;
color: #165dff;
}
&.disabled {
color: #c9cdd4;
cursor: not-allowed;
}
&.has-children {
padding-right: 8px;
}
}
.item-icon {
display: inline-flex;
width: 16px;
height: 16px;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-arrow {
color: #86909c;
font-size: 14px;
}
.context-menu-divider {
height: 1px;
margin: 4px 8px;
background: #e5e6eb;
}
.dark {
.global-context-menu {
background: #1f2937;
border-color: #374151;
}
.context-menu-item {
color: #e5e7eb;
&:hover:not(.disabled) {
background: #374151;
color: #93c5fd;
}
}
.context-menu-divider {
background: #374151;
}
}
</style>