'use client'; import * as React from 'react'; import { useState } from 'react'; import { Button } from '@nice/ui/components/button'; import { Badge } from '@nice/ui/components/badge'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors, closestCenter, DragOverEvent, } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { IconChevronRight, IconChevronDown, IconDots, IconEdit, IconTrash, IconPlus, IconGripVertical, } from '@tabler/icons-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@nice/ui/components/dropdown-menu'; import { TaxonomySlug, type TermWithTaxonomy } from '@fenghuo/common'; // 通用树节点接口 export interface TreeNode { id: string; name: string; slug?: string | null; description?: string | null; parentId?: string | null; level?: number; order?: number; // 添加排序字段 children?: TreeNode[]; terms?: TermWithTaxonomy[]; // 添加 terms 字段支持 [key: string]: any; // 允许额外的属性 } // 图标配置 export interface TreeIconConfig { // 根节点图标 rootIcon?: React.ComponentType<{ className?: string }>; // 父节点图标(有子节点) parentIcon?: React.ComponentType<{ className?: string }>; // 父节点展开时图标 parentExpandedIcon?: React.ComponentType<{ className?: string }>; // 叶子节点图标 leafIcon?: React.ComponentType<{ className?: string }>; // 根据节点动态决定图标的函数 getIcon?: (node: TreeNode, hasChildren: boolean, isExpanded: boolean) => React.ComponentType<{ className?: string }>; } // 操作配置 export interface TreeActionConfig { // 编辑操作 onEdit?: (node: T) => void; editLabel?: string; // 删除操作 onDelete?: (node: T) => void; deleteLabel?: string; // 添加子节点操作 onAddChild?: (parentId: string) => void; addChildLabel?: string; // 自定义操作 customActions?: Array<{ label: string; icon?: React.ComponentType<{ className?: string }>; onClick: (node: T) => void; className?: string; }>; } // 显示配置 export interface TreeDisplayConfig { // 是否显示描述 showDescription?: boolean; // 是否显示 slug showSlug?: boolean; // 自定义渲染节点内容 renderContent?: (node: T) => React.ReactNode; // 自定义渲染额外信息(如徽章) renderExtra?: (node: T) => React.ReactNode; // 节点选择样式配置 selectedClassName?: string; hoverClassName?: string; } // 拖拽配置 export interface TreeDragConfig { // 是否启用拖拽 enabled?: boolean; // 拖拽开始回调 onDragStart?: (event: DragStartEvent) => void; // 拖拽结束回调 - 返回更新后的数据 onDragEnd?: (oldData: T[], newData: T[]) => void; // 拖拽过程中回调 onDragOver?: (event: DragOverEvent) => void; // 是否显示拖拽手柄 showHandle?: boolean; } // 可拖拽的树节点组件属性 interface DraggableTreeNodeProps { node: T; level: number; expandedIds: Set; selectedNode?: T | null; onToggleExpand: (id: string) => void; onSelect?: (node: T) => void; iconConfig: TreeIconConfig; actionConfig: TreeActionConfig; displayConfig: TreeDisplayConfig; dragConfig: TreeDragConfig; indentSize?: number; isDragging?: boolean; siblings: T[]; } // 可拖拽的树节点组件 function DraggableTreeNode({ node, level, expandedIds, selectedNode, onToggleExpand, onSelect, iconConfig, actionConfig, displayConfig, dragConfig, indentSize = 20, isDragging = false, siblings, }: DraggableTreeNodeProps) { // 只有在启用拖拽时才使用 useSortable const sortableProps = dragConfig.enabled ? useSortable({ id: node.id }) : null; const { attributes = {}, listeners = {}, setNodeRef = () => { }, transform = null, transition = '', isDragging: isSortableDragging = false, } = sortableProps || {}; const style = dragConfig.enabled ? { transform: CSS.Transform.toString(transform), transition, } : {}; const hasChildren = node.children && node.children.length > 0; const isExpanded = expandedIds.has(node.id); const isSelected = selectedNode?.id === node.id; // 获取图标 const getNodeIcon = () => { if (iconConfig.getIcon) { return iconConfig.getIcon(node, !!hasChildren, isExpanded); } if (level === 0 && iconConfig.rootIcon) { return iconConfig.rootIcon; } if (hasChildren) { if (isExpanded && iconConfig.parentExpandedIcon) { return iconConfig.parentExpandedIcon; } return iconConfig.parentIcon; } return iconConfig.leafIcon; }; const NodeIcon = getNodeIcon(); // 默认选中样式 const defaultSelectedClassName = 'bg-primary/10 border border-primary/20'; const defaultHoverClassName = 'hover:bg-muted/50 border border-transparent'; const selectedClassName = displayConfig.selectedClassName || defaultSelectedClassName; const hoverClassName = displayConfig.hoverClassName || defaultHoverClassName; return (
onSelect?.(node)} style={{ paddingLeft: `${level * indentSize + 8}px` }} >
{/* 拖拽手柄 */} {dragConfig.enabled && dragConfig.showHandle && (
)} {/* 展开/折叠按钮 */}
{hasChildren ? ( ) : (
)}
{/* 图标 */} {NodeIcon && (
)} {/* 节点内容 */}
{displayConfig.renderContent ? ( displayConfig.renderContent(node) ) : ( <>
{node.name} {/* 显示 organization_type 类型的 term 作为标签 */} {node.terms?.filter(term => term.taxonomy.slug === TaxonomySlug.ORGANIZATION_TYPE).map(term => ( {term.name} ))} {displayConfig.renderExtra && displayConfig.renderExtra(node)}
{displayConfig.showDescription && node.description && (
{node.description}
)} {/* 显示 profession 类型的 term 作为文本 */} {(() => { const professionTerms = node.terms?.filter(term => term.taxonomy.slug === TaxonomySlug.PROFESSION) || []; return professionTerms.length > 0 && (
{professionTerms.map((term, index) => ( {index > 0 && '• '}{term.name} ))}
); })()} {displayConfig.showSlug && node.slug && (
/{node.slug}
)} )}
{/* 操作菜单 */} {(actionConfig.onEdit || actionConfig.onDelete || actionConfig.onAddChild || actionConfig.customActions) && (
{actionConfig.onEdit && ( { e.stopPropagation(); actionConfig.onEdit!(node); }} className="cursor-pointer" > {actionConfig.editLabel || '编辑'} )} {actionConfig.onAddChild && ( { e.stopPropagation(); actionConfig.onAddChild!(node.id); }} className="cursor-pointer" > {actionConfig.addChildLabel || '添加子项'} )} {actionConfig.customActions?.map((action, index) => ( { e.stopPropagation(); action.onClick(node); }} className={`cursor-pointer ${action.className || ''}`} > {action.icon && } {action.label} ))} {(actionConfig.onEdit || actionConfig.onAddChild || actionConfig.customActions) && actionConfig.onDelete && ( )} {actionConfig.onDelete && ( { e.stopPropagation(); actionConfig.onDelete!(node); }} className="text-destructive focus:text-destructive cursor-pointer" > {actionConfig.deleteLabel || '删除'} )}
)}
{/* 子节点 */} {hasChildren && isExpanded && (
)}
); } // 可拖拽的树级别组件 function DraggableTreeLevel({ nodes, level, expandedIds, selectedNode, onToggleExpand, onSelect, iconConfig, actionConfig, displayConfig, dragConfig, indentSize, }: { nodes: T[]; level: number; expandedIds: Set; selectedNode?: T | null; onToggleExpand: (id: string) => void; onSelect?: (node: T) => void; iconConfig: TreeIconConfig; actionConfig: TreeActionConfig; displayConfig: TreeDisplayConfig; dragConfig: TreeDragConfig; indentSize?: number; }) { // 按order排序 const sortedNodes = [...nodes].sort((a, b) => (a.order || 0) - (b.order || 0)); if (!dragConfig.enabled) { // 如果不启用拖拽,直接使用拖拽组件但不包装在SortableContext中 return (
{sortedNodes.map((node) => ( ))}
); } return ( node.id)} strategy={verticalListSortingStrategy}>
{sortedNodes.map((node) => ( ))}
); } // 树选择器组件属性 export interface TreeSelectorProps { // 数据 data: T[]; // 选中的节点 selectedNode?: T | null; // 选择回调 onSelect?: (node: T) => void; // 图标配置 iconConfig?: TreeIconConfig; // 操作配置 actionConfig?: TreeActionConfig; // 显示配置 displayConfig?: TreeDisplayConfig; // 拖拽配置 dragConfig?: TreeDragConfig; // 缩进大小 indentSize?: number; // 容器样式 className?: string; // 是否显示在容器中 containerized?: boolean; // 最大高度 maxHeight?: string; } // 工具函数:查找节点的同级节点ID function findSiblingNodes(nodes: T[], targetId: string): string[] { const siblings: string[] = []; function searchInLevel(levelNodes: T[]): boolean { // 检查当前层级是否包含目标节点 const hasTarget = levelNodes.some(node => node.id === targetId); if (hasTarget) { // 如果当前层级包含目标节点,返回所有同级节点的ID siblings.push(...levelNodes.map(node => node.id)); return true; } // 递归搜索子节点 for (const node of levelNodes) { if (node.children && searchInLevel(node.children as T[])) { return true; } } return false; } searchInLevel(nodes); return siblings; } // 支持 BigInt 的深拷贝函数 function deepClone(obj: T): T { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()) as T; } if (obj instanceof Array) { return obj.map(item => deepClone(item)) as T; } if (typeof obj === 'object') { const cloned = {} as T; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } return obj; } // 工具函数:更新节点的order值 function updateNodeOrder(nodes: T[], draggedId: string, targetId: string): T[] { const clonedNodes = deepClone(nodes); // 递归查找并更新节点 function updateInLevel(levelNodes: T[]): T[] { // 找到拖拽的节点和目标节点 const draggedIndex = levelNodes.findIndex(node => node.id === draggedId); const targetIndex = levelNodes.findIndex(node => node.id === targetId); if (draggedIndex !== -1 && targetIndex !== -1) { // 在同一层级内重新排列 const draggedNodes = levelNodes.splice(draggedIndex, 1); const draggedNode = draggedNodes[0]; if (draggedNode) { levelNodes.splice(targetIndex, 0, draggedNode); // 重新计算order值 levelNodes.forEach((node, index) => { node.order = index; }); } return levelNodes; } // 递归处理子节点 return levelNodes.map(node => ({ ...node, children: node.children ? updateInLevel(node.children as T[]) : node.children })); } return updateInLevel(clonedNodes); } // 主要的树选择器组件 export function TreeSelector({ data, selectedNode, onSelect, iconConfig = {}, actionConfig = {}, displayConfig = {}, dragConfig = {}, indentSize = 20, className = '', containerized = true, maxHeight = 'calc(100vh - 300px)', }: TreeSelectorProps) { const [expandedIds, setExpandedIds] = useState>(new Set()); const [activeId, setActiveId] = useState(null); // 设置传感器 const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ); const toggleExpand = (id: string) => { const newExpanded = new Set(expandedIds); if (newExpanded.has(id)) { newExpanded.delete(id); } else { newExpanded.add(id); } setExpandedIds(newExpanded); }; const handleDragStart = (event: DragStartEvent) => { const { active } = event; setActiveId(active.id as string); // 找到同级节点并收起它们 const siblingIds = findSiblingNodes(data, active.id as string); const newExpandedIds = new Set(expandedIds); // 移除同级节点(包括拖拽节点本身) siblingIds.forEach(id => { newExpandedIds.delete(id); }); setExpandedIds(newExpandedIds); if (dragConfig.onDragStart) { dragConfig.onDragStart(event); } }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveId(null); if (!over || active.id === over.id) { return; } // 更新数据 const newData = updateNodeOrder(data, active.id as string, over.id as string); // 调用回调 if (dragConfig.onDragEnd) { dragConfig.onDragEnd(data, newData); } }; const handleDragOver = (event: DragOverEvent) => { if (dragConfig.onDragOver) { dragConfig.onDragOver(event); } }; const content = (
); // 如果启用拖拽,使用DndContext包装 const wrappedContent = dragConfig.enabled ? ( {content} {activeId ? (
{/* 空的覆盖层,让拖拽时只显示原节点的虚化效果 */}
) : null}
) : content; if (containerized) { return (
{wrappedContent}
); } return
{wrappedContent}
; } // 导出默认组件 export default TreeSelector;