"use client"; import { useEffect, useRef } from "react"; export type CtxMenuItem = { key: string; label: string; icon?: React.ReactNode; danger?: boolean; disabled?: boolean; onClick: () => void; }; /** * 统一右键/长按浮层菜单(飞书式)。 * 定位会钳制在视口内;点击外侧或滚动关闭。 */ export default function ContextMenu({ x, y, items, onClose, }: { x: number; y: number; items: CtxMenuItem[]; onClose: () => void; }) { const ref = useRef(null); useEffect(() => { const close = () => onClose(); window.addEventListener("click", close); window.addEventListener("scroll", close, true); window.addEventListener("resize", close); return () => { window.removeEventListener("click", close); window.removeEventListener("scroll", close, true); window.removeEventListener("resize", close); }; }, [onClose]); useEffect(() => { const el = ref.current; if (!el) return; const rect = el.getBoundingClientRect(); let left = x; let top = y; if (left + rect.width > window.innerWidth - 8) { left = Math.max(8, window.innerWidth - rect.width - 8); } if (top + rect.height > window.innerHeight - 8) { top = Math.max(8, window.innerHeight - rect.height - 8); } el.style.left = `${left}px`; el.style.top = `${top}px`; }, [x, y, items]); return (
e.stopPropagation()} onContextMenu={(e) => e.preventDefault()} > {items.map((it) => ( ))}
); } /** 绑定桌面右键 + 移动端长按,打开菜单 */ export function usePressMenu( openAt: (x: number, y: number) => void ): { onContextMenu: (e: React.MouseEvent) => void; onTouchStart: (e: React.TouchEvent) => void; onTouchEnd: () => void; onTouchMove: () => void; } { const timer = useRef | null>(null); const clear = () => { if (timer.current) { clearTimeout(timer.current); timer.current = null; } }; return { onContextMenu: (e) => { e.preventDefault(); e.stopPropagation(); openAt(e.clientX, e.clientY); }, onTouchStart: (e) => { const t = e.touches[0]; if (!t) return; clear(); timer.current = setTimeout(() => { openAt(t.clientX, t.clientY); }, 480); }, onTouchEnd: clear, onTouchMove: clear, }; }