import type { Ref } from 'vue' import { ref } from 'vue' import type { DropdownInstance, TreeV2Instance } from 'element-plus' interface NodeBase { id: string; label: string; children?: T[] } type NodeType = T & NodeBase type LocateResult = { node: NodeType; parent: NodeType | null; container: NodeType[]; index: number } | null interface MenuItem { key: string; text: string; disabled?: boolean } interface ContextMenuConfig { dataRef: Ref[]>; treeRef: Ref; expandedKeysRef: Ref; locate: (id: string) => LocateResult; startEdit: (node: NodeType) => void; } interface ContextMenuContext { createNode: (prefix: string) => NodeType; setData: (next: NodeType[]) => void; expandNode: (id: string) => void; setCurrentKey: (id: string) => void; locate: (id: string) => LocateResult; startEdit: (node: NodeType) => void; dataRef: Ref[]>; } interface LevelConfig { depth: number addKey?: string addText?: string allowDelete?: boolean } interface HierarchyConfig { rootKey: string rootText: string levels: LevelConfig[] } interface ContextMenuHandler { getMenuItems: (node: NodeType | null, ctx: ContextMenuContext) => MenuItem[]; execute: (cmd: string, node: NodeType | null, ctx: ContextMenuContext) => Promise | void; } /** * 获取节点深度 */ const getDepth = (node: NodeType, ctx: ContextMenuContext): number => { const target = ctx.locate(node.id) if (!target) return -1 let depth = 0 let p = target.parent while (p) { depth += 1 const pt = ctx.locate(p.id) p = pt?.parent ?? null } return depth } /** * 层级化上下文菜单处理器 */ class HierarchyContextMenuHandler implements ContextMenuHandler { constructor(private config: HierarchyConfig) {} getMenuItems = (node: NodeType | null, ctx: ContextMenuContext): MenuItem[] => { // 空白区域右键 - 添加根节点 if (!node) { return [{ key: this.config.rootKey, text: this.config.rootText }] } const depth = getDepth(node, ctx) const levelConfig = this.config.levels.find(l => l.depth === depth) if (!levelConfig) { // 未配置的层级,只显示删除 return [{ key: 'delete', text: '删除' }] } const items: MenuItem[] = [] // 添加子级菜单项 if (levelConfig.addKey && levelConfig.addText) { items.push({ key: levelConfig.addKey, text: levelConfig.addText }) } // 删除菜单项 if (levelConfig.allowDelete !== false) { items.push({ key: 'delete', text: '删除' }) } return items } execute = async (cmd: string, node: NodeType | null, ctx: ContextMenuContext) => { // 添加根节点 if (!node && cmd === this.config.rootKey) { const next = ctx.createNode('root') next.label = this.config.rootText.replace('添加', '') ctx.setData([...ctx.dataRef.value, next]) return } if (!node) return // 删除节点 if (cmd === 'delete') { const target = ctx.locate(node.id) if (!target) return target.container.splice(target.index, 1) ctx.setData([...ctx.dataRef.value]) return } // 查找匹配的层级配置 const depth = getDepth(node, ctx) const levelConfig = this.config.levels.find(l => l.depth === depth && l.addKey === cmd) if (levelConfig) { const target = ctx.locate(node.id) if (!target) return const next = ctx.createNode(node.id) next.label = levelConfig.addText?.replace('添加', '') || '新目录' target.node.children = target.node.children || [] target.node.children.push(next) ctx.setData([...ctx.dataRef.value]) ctx.expandNode(target.node.id) ctx.setCurrentKey(next.id) } } } /** * 默认上下文菜单处理器 */ class DefaultContextMenuHandler implements ContextMenuHandler { getMenuItems = (node: NodeType | null, _ctx: ContextMenuContext): MenuItem[] => { if (!node) return [{ key: 'add-root', text: '添加根目录' }] return [ { key: 'add-sibling', text: '添加目录' }, { key: 'add-child', text: '添加子目录' }, { key: 'rename', text: '重命名' }, { key: 'delete', text: '删除' }, ] } execute = async (cmd: string, node: NodeType | null, ctx: ContextMenuContext) => { if (!node && cmd === 'add-root') { ctx.setData([...ctx.dataRef.value, ctx.createNode('node')]) return } if (!node) return if (cmd === 'add-sibling') { const target = ctx.locate(node.id) if (!target) return const prefix = target.parent ? target.parent.id : 'node' target.container.push(ctx.createNode(prefix)) ctx.setData([...ctx.dataRef.value]) return } if (cmd === 'add-child') { const target = ctx.locate(node.id) if (!target) return target.node.children = target.node.children || [] target.node.children.push(ctx.createNode(target.node.id)) ctx.setData([...ctx.dataRef.value]) ctx.expandNode(target.node.id) ctx.setCurrentKey(target.node.id) return } if (cmd === 'rename') { ctx.startEdit(node); return } if (cmd === 'delete') { const target = ctx.locate(node.id) if (!target) return target.container.splice(target.index, 1) ctx.setData([...ctx.dataRef.value]) } } } class DbTreeContextMenu { dropdownRef = ref() position = ref({ top: 0, left: 0, bottom: 0, right: 0 } as DOMRect) triggerRef = ref({ getBoundingClientRect: () => this.position.value }) currentNode = ref | null>(null) private config: ContextMenuConfig private handler: ContextMenuHandler constructor(config: ContextMenuConfig, handler?: ContextMenuHandler) { this.config = config this.handler = handler ?? new DefaultContextMenuHandler() } private createNode = (prefix: string): NodeType => { const suffix = Math.random().toString(36).slice(2, 8) const id = `${prefix}-${suffix}` return { id, label: '新目录' } as NodeType } private setData = (next: NodeType[]) => { this.config.dataRef.value = next } private expandNode = (id: string) => { const keys = Array.from(new Set([...this.config.expandedKeysRef.value, id])) this.config.expandedKeysRef.value = keys this.config.treeRef.value?.setExpandedKeys(keys) } private setCurrentKey = (id: string) => { this.config.treeRef.value?.setCurrentKey?.(id) } private ctx = (): ContextMenuContext => ({ createNode: this.createNode, setData: this.setData, expandNode: this.expandNode, setCurrentKey: this.setCurrentKey, locate: this.config.locate, startEdit: this.config.startEdit, dataRef: this.config.dataRef, }) getMenuItems = (): MenuItem[] => this.handler.getMenuItems(this.currentNode.value, this.ctx()) openContextMenu = (event: MouseEvent, nodeData: NodeType) => { const { clientX, clientY } = event this.position.value = DOMRect.fromRect({ x: clientX, y: clientY }) event.preventDefault() this.currentNode.value = nodeData this.dropdownRef.value?.handleOpen() } openBlankContextMenu = (event: MouseEvent) => { const { clientX, clientY } = event this.position.value = DOMRect.fromRect({ x: clientX, y: clientY }) event.preventDefault() this.currentNode.value = null this.dropdownRef.value?.handleOpen() } closeContextMenu = () => { this.dropdownRef.value?.handleClose() } onCommand = async (cmd: string) => { await this.handler.execute(cmd, this.currentNode.value, this.ctx()) } onGlobalCommand = (cmd: string) => { void this.handler.execute(cmd, this.currentNode.value, this.ctx()) } } /** * 创建层级化的上下文菜单处理器 * @param config 层级配置 * @returns ContextMenuHandler */ const createHierarchyContextMenuHandler = (config: HierarchyConfig): ContextMenuHandler => { return new HierarchyContextMenuHandler(config) } const createContextMenu = (config: ContextMenuConfig, handler?: ContextMenuHandler | HierarchyConfig) => { // 如果传入的是 HierarchyConfig,自动创建 HierarchyContextMenuHandler if (handler && 'rootKey' in handler && 'rootText' in handler && 'levels' in handler) { return new DbTreeContextMenu(config, createHierarchyContextMenuHandler(handler)) } return new DbTreeContextMenu(config, handler as ContextMenuHandler | undefined) } export { DbTreeContextMenu, HierarchyContextMenuHandler, createHierarchyContextMenuHandler } export type { ContextMenuConfig, ContextMenuHandler, MenuItem, NodeType, LocateResult, HierarchyConfig, LevelConfig } export const useContextMenu = (config: ContextMenuConfig, handler?: ContextMenuHandler | HierarchyConfig) => createContextMenu(config, handler)