| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue' |
| | | |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { useImUiStore } from '../store/uiStore' |
| | | |
| | | defineOptions({ name: 'ImContextMenu' }) |
| | | |
| | | const uiStore = useImUiStore() |
| | | const contextMenu = computed(() => uiStore.contextMenu) |
| | | |
| | | /** |
| | | * 计ç®èåå®é
渲æåæ ï¼é è¿è§å£å³ / ä¸è¾¹ç¼æ¶åå¼¹ï¼é¿å
èå被è£åª |
| | | * |
| | | * itemHeight / menuWidth æ¯å模æ¿é px-4 py-2 + text-13px / min-w-30 é
å¥çå®é
å°ºå¯¸ï¼ |
| | | * dividerHeight = 9pxï¼my-1 ä¸ä¸å 4 + 1px borderï¼ï¼ä»
éé¦é¡¹ç divided 计å
¥ï¼ |
| | | * menuHeight é¢å¤å 8 æ¯å¤å± py-1 çä¸ä¸ padding ä¹åï¼4px à 2ï¼ |
| | | */ |
| | | const adjustedPosition = computed(() => { |
| | | const items = contextMenu.value.items |
| | | const itemHeight = 34 |
| | | const dividerCount = items.filter((it, i) => it.divided && i > 0).length |
| | | const menuHeight = items.length * itemHeight + dividerCount * 9 + 8 |
| | | const menuWidth = 120 |
| | | let x = contextMenu.value.position.x |
| | | let y = contextMenu.value.position.y |
| | | // SSR å
åºï¼window ä¸å¯ç¨æ¶ç´æ¥è¿ååå§åæ |
| | | if (typeof window !== 'undefined') { |
| | | if (y + menuHeight > window.innerHeight) { |
| | | y = window.innerHeight - menuHeight |
| | | } |
| | | if (x + menuWidth > window.innerWidth) { |
| | | x = window.innerWidth - menuWidth |
| | | } |
| | | } |
| | | // è§å£å¾å° / èå项å¾å¤æ¶ä¸é¢åæ³ä¼ç®åºè´å¼ï¼æèåé¡¶ / 左边æ¨å° 0 å
åº |
| | | return { x: Math.max(0, x), y: Math.max(0, y) } |
| | | }) |
| | | |
| | | type MenuItem = (typeof contextMenu.value.items)[number] |
| | | |
| | | /** éä¸èå项ï¼disabled 项忽ç¥ï¼æ£å¸¸é¡¹è° onSelect åè°åå
³éèå */ |
| | | function handleSelect(item: MenuItem) { |
| | | if (item.disabled) { |
| | | return |
| | | } |
| | | uiStore.contextMenu.onSelect?.(item) |
| | | uiStore.closeContextMenu() |
| | | } |
| | | |
| | | /** å
³éèåï¼ç¹é®ç½© / å¨é®ç½©ä¸å次å³é®é½ä¼è§¦å */ |
| | | function handleClose() { |
| | | uiStore.closeContextMenu() |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | éç¨å³é®èå |
| | | ç± useImUiStore.openContextMenu(position, items, onSelect) 触åå
¨å±åä¾å±ç¤º |
| | | è°ç¨æ¹å¨ @contextmenu.prevent äºä»¶éè° openContextMenu å³å¯ï¼ä¸éè¦èªå·±æç»ä»¶ |
| | | --> |
| | | <teleport to="body"> |
| | | <div |
| | | v-if="contextMenu.show" |
| | | class="fixed inset-0 z-9999" |
| | | @click.stop="handleClose" |
| | | @contextmenu.prevent="handleClose" |
| | | > |
| | | <div |
| | | class="fixed min-w-30 py-1 bg-[var(--ant-color-bg-elevated)] rounded-md shadow-lg" |
| | | :style="{ left: `${adjustedPosition.x }px`, top: `${adjustedPosition.y }px` }" |
| | | > |
| | | <template v-for="(item, index) in contextMenu.items" :key="item.key"> |
| | | <!-- divided 项䏿¹æä¸æ¡åå²çº¿ï¼é¦é¡¹è·³è¿ï¼é¿å
空ç½ï¼ --> |
| | | <div |
| | | v-if="item.divided && index > 0" |
| | | class="my-1 mx-2 h-[1px] bg-[var(--ant-color-border-secondary)]" |
| | | ></div> |
| | | <div |
| | | class="flex gap-2 items-center px-4 py-2 text-13px text-left cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]" |
| | | :class="[ |
| | | item.disabled |
| | | ? '!text-[var(--ant-color-text-disabled)] cursor-not-allowed hover:!bg-transparent' |
| | | : item.danger |
| | | ? 'text-[#f56c6c]' |
| | | : 'text-[var(--ant-color-text)]' |
| | | ]" |
| | | @click.stop="handleSelect(item)" |
| | | > |
| | | <Icon v-if="item.icon" :icon="item.icon" :size="14" /> |
| | | <span>{{ item.name }}</span> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </teleport> |
| | | </template> |