720 lines
19 KiB
TypeScript
Executable File
720 lines
19 KiB
TypeScript
Executable File
'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<T extends TreeNode> {
|
||
// 编辑操作
|
||
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<T extends TreeNode> {
|
||
// 是否显示描述
|
||
showDescription?: boolean;
|
||
// 是否显示 slug
|
||
showSlug?: boolean;
|
||
// 自定义渲染节点内容
|
||
renderContent?: (node: T) => React.ReactNode;
|
||
// 自定义渲染额外信息(如徽章)
|
||
renderExtra?: (node: T) => React.ReactNode;
|
||
// 节点选择样式配置
|
||
selectedClassName?: string;
|
||
hoverClassName?: string;
|
||
}
|
||
|
||
// 拖拽配置
|
||
export interface TreeDragConfig<T extends TreeNode> {
|
||
// 是否启用拖拽
|
||
enabled?: boolean;
|
||
// 拖拽开始回调
|
||
onDragStart?: (event: DragStartEvent) => void;
|
||
// 拖拽结束回调 - 返回更新后的数据
|
||
onDragEnd?: (oldData: T[], newData: T[]) => void;
|
||
// 拖拽过程中回调
|
||
onDragOver?: (event: DragOverEvent) => void;
|
||
// 是否显示拖拽手柄
|
||
showHandle?: boolean;
|
||
}
|
||
|
||
// 可拖拽的树节点组件属性
|
||
interface DraggableTreeNodeProps<T extends TreeNode> {
|
||
node: T;
|
||
level: number;
|
||
expandedIds: Set<string>;
|
||
selectedNode?: T | null;
|
||
onToggleExpand: (id: string) => void;
|
||
onSelect?: (node: T) => void;
|
||
iconConfig: TreeIconConfig;
|
||
actionConfig: TreeActionConfig<T>;
|
||
displayConfig: TreeDisplayConfig<T>;
|
||
dragConfig: TreeDragConfig<T>;
|
||
indentSize?: number;
|
||
isDragging?: boolean;
|
||
siblings: T[];
|
||
}
|
||
|
||
// 可拖拽的树节点组件
|
||
function DraggableTreeNode<T extends TreeNode>({
|
||
node,
|
||
level,
|
||
expandedIds,
|
||
selectedNode,
|
||
onToggleExpand,
|
||
onSelect,
|
||
iconConfig,
|
||
actionConfig,
|
||
displayConfig,
|
||
dragConfig,
|
||
indentSize = 20,
|
||
isDragging = false,
|
||
siblings,
|
||
}: DraggableTreeNodeProps<T>) {
|
||
// 只有在启用拖拽时才使用 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 (
|
||
<div className="select-none" style={style} ref={setNodeRef}>
|
||
<div
|
||
className={`flex items-center justify-between p-2 rounded-md group cursor-pointer transition-colors ${isSelected ? selectedClassName : hoverClassName
|
||
} ${isSortableDragging ? 'opacity-50' : ''} ${isDragging ? 'z-50' : ''}`}
|
||
onClick={() => onSelect?.(node)}
|
||
style={{ paddingLeft: `${level * indentSize + 8}px` }}
|
||
>
|
||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||
{/* 拖拽手柄 */}
|
||
{dragConfig.enabled && dragConfig.showHandle && (
|
||
<div
|
||
className="w-4 h-4 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"
|
||
{...attributes}
|
||
{...listeners}
|
||
>
|
||
<IconGripVertical className="h-3 w-3 text-muted-foreground" />
|
||
</div>
|
||
)}
|
||
|
||
{/* 展开/折叠按钮 */}
|
||
<div className="w-5 h-5 flex items-center justify-center">
|
||
{hasChildren ? (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-4 w-4 p-0 hover:bg-background"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onToggleExpand(node.id);
|
||
}}
|
||
>
|
||
{isExpanded ? (
|
||
<IconChevronDown className="h-3 w-3" />
|
||
) : (
|
||
<IconChevronRight className="h-3 w-3" />
|
||
)}
|
||
</Button>
|
||
) : (
|
||
<div className="w-3 h-3" />
|
||
)}
|
||
</div>
|
||
|
||
{/* 图标 */}
|
||
{NodeIcon && (
|
||
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
|
||
<NodeIcon className="h-4 w-4" />
|
||
</div>
|
||
)}
|
||
|
||
{/* 节点内容 */}
|
||
<div className="flex-1 min-w-0">
|
||
{displayConfig.renderContent ? (
|
||
displayConfig.renderContent(node)
|
||
) : (
|
||
<>
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="font-medium text-sm truncate">{node.name}</span>
|
||
{/* 显示 organization_type 类型的 term 作为标签 */}
|
||
{node.terms?.filter(term => term.taxonomy.slug === TaxonomySlug.ORGANIZATION_TYPE).map(term => (
|
||
<Badge key={term.id} variant="secondary" className="text-xs shrink-0">
|
||
{term.name}
|
||
</Badge>
|
||
))}
|
||
{displayConfig.renderExtra && displayConfig.renderExtra(node)}
|
||
</div>
|
||
{displayConfig.showDescription && node.description && (
|
||
<div className="text-xs text-muted-foreground mt-1 truncate">
|
||
{node.description}
|
||
</div>
|
||
)}
|
||
{/* 显示 profession 类型的 term 作为文本 */}
|
||
{(() => {
|
||
const professionTerms = node.terms?.filter(term => term.taxonomy.slug === TaxonomySlug.PROFESSION) || [];
|
||
return professionTerms.length > 0 && (
|
||
<div className="mt-1 flex flex-wrap gap-1">
|
||
{professionTerms.map((term, index) => (
|
||
<span key={term.id} className="text-xs text-muted-foreground">
|
||
{index > 0 && '• '}{term.name}
|
||
</span>
|
||
))}
|
||
</div>
|
||
);
|
||
})()}
|
||
{displayConfig.showSlug && node.slug && (
|
||
<div className="text-xs text-gray-400 dark:text-gray-500 mt-1">/{node.slug}</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作菜单 */}
|
||
{(actionConfig.onEdit || actionConfig.onDelete || actionConfig.onAddChild || actionConfig.customActions) && (
|
||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<IconDots className="h-4 w-4" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end">
|
||
{actionConfig.onEdit && (
|
||
<DropdownMenuItem
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
actionConfig.onEdit!(node);
|
||
}}
|
||
className="cursor-pointer"
|
||
>
|
||
<IconEdit className="mr-2 h-4 w-4" />
|
||
{actionConfig.editLabel || '编辑'}
|
||
</DropdownMenuItem>
|
||
)}
|
||
{actionConfig.onAddChild && (
|
||
<DropdownMenuItem
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
actionConfig.onAddChild!(node.id);
|
||
}}
|
||
className="cursor-pointer"
|
||
>
|
||
<IconPlus className="mr-2 h-4 w-4" />
|
||
{actionConfig.addChildLabel || '添加子项'}
|
||
</DropdownMenuItem>
|
||
)}
|
||
{actionConfig.customActions?.map((action, index) => (
|
||
<DropdownMenuItem
|
||
key={index}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
action.onClick(node);
|
||
}}
|
||
className={`cursor-pointer ${action.className || ''}`}
|
||
>
|
||
{action.icon && <action.icon className="mr-2 h-4 w-4" />}
|
||
{action.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
{(actionConfig.onEdit || actionConfig.onAddChild || actionConfig.customActions) && actionConfig.onDelete && (
|
||
<DropdownMenuSeparator />
|
||
)}
|
||
{actionConfig.onDelete && (
|
||
<DropdownMenuItem
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
actionConfig.onDelete!(node);
|
||
}}
|
||
className="text-destructive focus:text-destructive cursor-pointer"
|
||
>
|
||
<IconTrash className="mr-2 h-4 w-4" />
|
||
{actionConfig.deleteLabel || '删除'}
|
||
</DropdownMenuItem>
|
||
)}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 子节点 */}
|
||
{hasChildren && isExpanded && (
|
||
<div className="ml-1">
|
||
<DraggableTreeLevel
|
||
nodes={node.children! as T[]}
|
||
level={level + 1}
|
||
expandedIds={expandedIds}
|
||
selectedNode={selectedNode}
|
||
onToggleExpand={onToggleExpand}
|
||
onSelect={onSelect}
|
||
iconConfig={iconConfig}
|
||
actionConfig={actionConfig}
|
||
displayConfig={displayConfig}
|
||
dragConfig={dragConfig}
|
||
indentSize={indentSize}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 可拖拽的树级别组件
|
||
function DraggableTreeLevel<T extends TreeNode>({
|
||
nodes,
|
||
level,
|
||
expandedIds,
|
||
selectedNode,
|
||
onToggleExpand,
|
||
onSelect,
|
||
iconConfig,
|
||
actionConfig,
|
||
displayConfig,
|
||
dragConfig,
|
||
indentSize,
|
||
}: {
|
||
nodes: T[];
|
||
level: number;
|
||
expandedIds: Set<string>;
|
||
selectedNode?: T | null;
|
||
onToggleExpand: (id: string) => void;
|
||
onSelect?: (node: T) => void;
|
||
iconConfig: TreeIconConfig;
|
||
actionConfig: TreeActionConfig<T>;
|
||
displayConfig: TreeDisplayConfig<T>;
|
||
dragConfig: TreeDragConfig<T>;
|
||
indentSize?: number;
|
||
}) {
|
||
// 按order排序
|
||
const sortedNodes = [...nodes].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||
|
||
if (!dragConfig.enabled) {
|
||
// 如果不启用拖拽,直接使用拖拽组件但不包装在SortableContext中
|
||
return (
|
||
<div className="space-y-1">
|
||
{sortedNodes.map((node) => (
|
||
<DraggableTreeNode
|
||
key={node.id}
|
||
node={node}
|
||
level={level}
|
||
expandedIds={expandedIds}
|
||
selectedNode={selectedNode}
|
||
onToggleExpand={onToggleExpand}
|
||
onSelect={onSelect}
|
||
iconConfig={iconConfig}
|
||
actionConfig={actionConfig}
|
||
displayConfig={displayConfig}
|
||
dragConfig={dragConfig}
|
||
indentSize={indentSize}
|
||
siblings={sortedNodes}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<SortableContext items={sortedNodes.map(node => node.id)} strategy={verticalListSortingStrategy}>
|
||
<div className="space-y-1">
|
||
{sortedNodes.map((node) => (
|
||
<DraggableTreeNode
|
||
key={node.id}
|
||
node={node}
|
||
level={level}
|
||
expandedIds={expandedIds}
|
||
selectedNode={selectedNode}
|
||
onToggleExpand={onToggleExpand}
|
||
onSelect={onSelect}
|
||
iconConfig={iconConfig}
|
||
actionConfig={actionConfig}
|
||
displayConfig={displayConfig}
|
||
dragConfig={dragConfig}
|
||
indentSize={indentSize}
|
||
siblings={sortedNodes}
|
||
/>
|
||
))}
|
||
</div>
|
||
</SortableContext>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
// 树选择器组件属性
|
||
export interface TreeSelectorProps<T extends TreeNode> {
|
||
// 数据
|
||
data: T[];
|
||
// 选中的节点
|
||
selectedNode?: T | null;
|
||
// 选择回调
|
||
onSelect?: (node: T) => void;
|
||
// 图标配置
|
||
iconConfig?: TreeIconConfig;
|
||
// 操作配置
|
||
actionConfig?: TreeActionConfig<T>;
|
||
// 显示配置
|
||
displayConfig?: TreeDisplayConfig<T>;
|
||
// 拖拽配置
|
||
dragConfig?: TreeDragConfig<T>;
|
||
// 缩进大小
|
||
indentSize?: number;
|
||
// 容器样式
|
||
className?: string;
|
||
// 是否显示在容器中
|
||
containerized?: boolean;
|
||
// 最大高度
|
||
maxHeight?: string;
|
||
}
|
||
|
||
// 工具函数:查找节点的同级节点ID
|
||
function findSiblingNodes<T extends TreeNode>(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<T>(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<T extends TreeNode>(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<T extends TreeNode>({
|
||
data,
|
||
selectedNode,
|
||
onSelect,
|
||
iconConfig = {},
|
||
actionConfig = {},
|
||
displayConfig = {},
|
||
dragConfig = {},
|
||
indentSize = 20,
|
||
className = '',
|
||
containerized = true,
|
||
maxHeight = 'calc(100vh - 300px)',
|
||
}: TreeSelectorProps<T>) {
|
||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||
const [activeId, setActiveId] = useState<string | null>(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 = (
|
||
<div className="space-y-1">
|
||
<DraggableTreeLevel
|
||
nodes={data}
|
||
level={0}
|
||
expandedIds={expandedIds}
|
||
selectedNode={selectedNode}
|
||
onToggleExpand={toggleExpand}
|
||
onSelect={onSelect}
|
||
iconConfig={iconConfig}
|
||
actionConfig={actionConfig}
|
||
displayConfig={displayConfig}
|
||
dragConfig={dragConfig}
|
||
indentSize={indentSize}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
// 如果启用拖拽,使用DndContext包装
|
||
const wrappedContent = dragConfig.enabled ? (
|
||
<DndContext
|
||
sensors={sensors}
|
||
collisionDetection={closestCenter}
|
||
onDragStart={handleDragStart}
|
||
onDragEnd={handleDragEnd}
|
||
onDragOver={handleDragOver}
|
||
>
|
||
{content}
|
||
<DragOverlay>
|
||
{activeId ? (
|
||
<div className="opacity-0">
|
||
{/* 空的覆盖层,让拖拽时只显示原节点的虚化效果 */}
|
||
</div>
|
||
) : null}
|
||
</DragOverlay>
|
||
</DndContext>
|
||
) : content;
|
||
|
||
if (containerized) {
|
||
return (
|
||
<div className={`p-4 ${className}`}>
|
||
<div className={`overflow-y-auto`} style={{ maxHeight }}>
|
||
{wrappedContent}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return <div className={className}>{wrappedContent}</div>;
|
||
}
|
||
|
||
// 导出默认组件
|
||
export default TreeSelector;
|