AreaTree.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import React from 'react';
  2. import { ChevronRight, ChevronDown, Folder, FolderOpen } from 'lucide-react';
  3. import { Button } from '@/client/components/ui/button';
  4. import { Badge } from '@/client/components/ui/badge';
  5. import { cn } from '@/client/lib/utils';
  6. interface AreaNode {
  7. id: number;
  8. name: string;
  9. code: string;
  10. level: number;
  11. parentId: number | null;
  12. isDisabled: number;
  13. children?: AreaNode[];
  14. }
  15. interface AreaTreeProps {
  16. areas: AreaNode[];
  17. expandedNodes: Set<number>;
  18. onToggleNode: (nodeId: number) => void;
  19. onEdit: (area: AreaNode) => void;
  20. onDelete: (area: AreaNode) => void;
  21. onToggleStatus: (area: AreaNode) => void;
  22. }
  23. export const AreaTree: React.FC<AreaTreeProps> = ({
  24. areas,
  25. expandedNodes,
  26. onToggleNode,
  27. onEdit,
  28. onDelete,
  29. onToggleStatus
  30. }) => {
  31. const renderTreeNode = (node: AreaNode, depth = 0) => {
  32. const hasChildren = node.children && node.children.length > 0;
  33. const isExpanded = expandedNodes.has(node.id);
  34. const isDisabled = node.isDisabled === 1;
  35. return (
  36. <div key={node.id} className="select-none">
  37. {/* 节点行 */}
  38. <div
  39. className={cn(
  40. "flex items-center gap-2 py-2 px-3 hover:bg-muted/50 cursor-pointer border-b",
  41. depth > 0 && "ml-6"
  42. )}
  43. style={{ marginLeft: `${depth * 24}px` }}
  44. >
  45. {/* 展开/收起按钮 */}
  46. {hasChildren && (
  47. <Button
  48. variant="ghost"
  49. size="sm"
  50. className="h-6 w-6 p-0"
  51. onClick={() => onToggleNode(node.id)}
  52. >
  53. {isExpanded ? (
  54. <ChevronDown className="h-4 w-4" />
  55. ) : (
  56. <ChevronRight className="h-4 w-4" />
  57. )}
  58. </Button>
  59. )}
  60. {!hasChildren && <div className="w-6" />}
  61. {/* 图标 */}
  62. <div className="flex-shrink-0">
  63. {hasChildren ? (
  64. isExpanded ? (
  65. <FolderOpen className="h-4 w-4 text-blue-500" />
  66. ) : (
  67. <Folder className="h-4 w-4 text-blue-400" />
  68. )
  69. ) : (
  70. <div className="h-4 w-4" />
  71. )}
  72. </div>
  73. {/* 节点信息 */}
  74. <div className="flex-1 flex items-center gap-3">
  75. <span className={cn("font-medium", isDisabled && "text-muted-foreground line-through")}>
  76. {node.name}
  77. </span>
  78. <Badge variant="outline" className="text-xs">
  79. {getLevelName(node.level)}
  80. </Badge>
  81. <span className="text-xs text-muted-foreground">
  82. {node.code}
  83. </span>
  84. <Badge variant={isDisabled ? "secondary" : "default"} className="text-xs">
  85. {isDisabled ? '禁用' : '启用'}
  86. </Badge>
  87. </div>
  88. {/* 操作按钮 */}
  89. <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
  90. <Button
  91. variant="ghost"
  92. size="sm"
  93. onClick={(e) => {
  94. e.stopPropagation();
  95. onEdit(node);
  96. }}
  97. >
  98. 编辑
  99. </Button>
  100. <Button
  101. variant="ghost"
  102. size="sm"
  103. onClick={(e) => {
  104. e.stopPropagation();
  105. onToggleStatus(node);
  106. }}
  107. >
  108. {isDisabled ? '启用' : '禁用'}
  109. </Button>
  110. <Button
  111. variant="ghost"
  112. size="sm"
  113. onClick={(e) => {
  114. e.stopPropagation();
  115. onDelete(node);
  116. }}
  117. >
  118. 删除
  119. </Button>
  120. </div>
  121. </div>
  122. {/* 子节点 */}
  123. {isExpanded && hasChildren && (
  124. <div>
  125. {node.children!.map(child => renderTreeNode(child, depth + 1))}
  126. </div>
  127. )}
  128. </div>
  129. );
  130. };
  131. return (
  132. <div className="border rounded-lg bg-background">
  133. {areas.map(area => renderTreeNode(area))}
  134. </div>
  135. );
  136. };
  137. // 获取层级显示名称
  138. const getLevelName = (level: number) => {
  139. switch (level) {
  140. case 1: return '省/直辖市';
  141. case 2: return '市';
  142. case 3: return '区/县';
  143. default: return '未知';
  144. }
  145. };