Files
yihuiyong-ui/apps/web-ele/src/components/db-tree/contextMenu.ts
2025-12-18 16:37:33 +08:00

269 lines
8.8 KiB
TypeScript
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.
import type { Ref } from 'vue'
import { ref } from 'vue'
import type { DropdownInstance, TreeV2Instance } from 'element-plus'
interface NodeBase<T> { id: string; label: string; children?: T[] }
type NodeType<T> = T & NodeBase<T>
type LocateResult<T> = { node: NodeType<T>; parent: NodeType<T> | null; container: NodeType<T>[]; index: number } | null
interface MenuItem { key: string; text: string; disabled?: boolean }
interface ContextMenuConfig<T> {
dataRef: Ref<NodeType<T>[]>;
treeRef: Ref<TreeV2Instance | undefined>;
expandedKeysRef: Ref<string[]>;
locate: (id: string) => LocateResult<T>;
startEdit: (node: NodeType<T>) => void;
}
interface ContextMenuContext<T> {
createNode: (prefix: string) => NodeType<T>;
setData: (next: NodeType<T>[]) => void;
expandNode: (id: string) => void;
setCurrentKey: (id: string) => void;
locate: (id: string) => LocateResult<T>;
startEdit: (node: NodeType<T>) => void;
dataRef: Ref<NodeType<T>[]>;
}
interface LevelConfig {
depth: number
addKey?: string
addText?: string
allowDelete?: boolean
}
interface HierarchyConfig {
rootKey: string
rootText: string
levels: LevelConfig[]
}
interface ContextMenuHandler<T> {
getMenuItems: (node: NodeType<T> | null, ctx: ContextMenuContext<T>) => MenuItem[];
execute: (cmd: string, node: NodeType<T> | null, ctx: ContextMenuContext<T>) => Promise<void> | void;
}
/**
* 获取节点深度
*/
const getDepth = <T>(node: NodeType<T>, ctx: ContextMenuContext<T>): 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<T> implements ContextMenuHandler<T> {
constructor(private config: HierarchyConfig) {}
getMenuItems = (node: NodeType<T> | null, ctx: ContextMenuContext<T>): 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<T> | null, ctx: ContextMenuContext<T>) => {
// 添加根节点
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<T> implements ContextMenuHandler<T> {
getMenuItems = (node: NodeType<T> | null, _ctx: ContextMenuContext<T>): 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<T> | null, ctx: ContextMenuContext<T>) => {
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<T> {
dropdownRef = ref<DropdownInstance>()
position = ref({ top: 0, left: 0, bottom: 0, right: 0 } as DOMRect)
triggerRef = ref({ getBoundingClientRect: () => this.position.value })
currentNode = ref<NodeType<T> | null>(null)
private config: ContextMenuConfig<T>
private handler: ContextMenuHandler<T>
constructor(config: ContextMenuConfig<T>, handler?: ContextMenuHandler<T>) {
this.config = config
this.handler = handler ?? new DefaultContextMenuHandler<T>()
}
private createNode = (prefix: string): NodeType<T> => {
const suffix = Math.random().toString(36).slice(2, 8)
const id = `${prefix}-${suffix}`
return { id, label: '新目录' } as NodeType<T>
}
private setData = (next: NodeType<T>[]) => { 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<T> => ({
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<T>) => {
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 = <T>(config: HierarchyConfig): ContextMenuHandler<T> => {
return new HierarchyContextMenuHandler<T>(config)
}
const createContextMenu = <T>(config: ContextMenuConfig<T>, handler?: ContextMenuHandler<T> | HierarchyConfig) => {
// 如果传入的是 HierarchyConfig自动创建 HierarchyContextMenuHandler
if (handler && 'rootKey' in handler && 'rootText' in handler && 'levels' in handler) {
return new DbTreeContextMenu<T>(config, createHierarchyContextMenuHandler<T>(handler))
}
return new DbTreeContextMenu<T>(config, handler as ContextMenuHandler<T> | undefined)
}
export { DbTreeContextMenu, HierarchyContextMenuHandler, createHierarchyContextMenuHandler }
export type { ContextMenuConfig, ContextMenuHandler, MenuItem, NodeType, LocateResult, HierarchyConfig, LevelConfig }
export const useContextMenu = <T>(config: ContextMenuConfig<T>, handler?: ContextMenuHandler<T> | HierarchyConfig) => createContextMenu<T>(config, handler)