AreaTreeAsync.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import React from 'react';
  2. import { ChevronRight, ChevronDown, Folder, FolderOpen, Loader2 } from 'lucide-react';
  3. import { Button } from '@d8d/shared-ui-components/components/ui/button';
  4. import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
  5. import { cn } from '@d8d/shared-ui-components/utils';
  6. import { useQuery } from '@tanstack/react-query';
  7. import { areaClientManager } from '../api/areaClient';
  8. import type { AreaNode } from '../types/area';
  9. interface AreaTreeAsyncProps {
  10. areas: AreaNode[];
  11. expandedNodes: Set<number>;
  12. onToggleNode: (nodeId: number) => void;
  13. onEdit: (area: AreaNode) => void;
  14. onDelete: (area: AreaNode) => void;
  15. onToggleStatus: (area: AreaNode) => void;
  16. onAddChild: (area: AreaNode) => void;
  17. }
  18. // 子树加载组件
  19. interface SubTreeLoaderProps {
  20. nodeId: number;
  21. isExpanded: boolean;
  22. hasChildren: boolean;
  23. depth: number;
  24. expandedNodes: Set<number>;
  25. onToggleNode: (nodeId: number) => void;
  26. onEdit: (area: AreaNode) => void;
  27. onDelete: (area: AreaNode) => void;
  28. onToggleStatus: (area: AreaNode) => void;
  29. onAddChild: (area: AreaNode) => void;
  30. }
  31. const SubTreeLoader: React.FC<SubTreeLoaderProps> = ({
  32. nodeId,
  33. isExpanded,
  34. hasChildren,
  35. depth,
  36. expandedNodes,
  37. onToggleNode,
  38. onEdit,
  39. onDelete,
  40. onToggleStatus,
  41. onAddChild
  42. }) => {
  43. const { data: subTreeData, isLoading: isSubTreeLoading } = useQuery({
  44. queryKey: ['areas-subtree', nodeId],
  45. queryFn: async () => {
  46. const res = await areaClientManager.get().index.$get({
  47. query: {
  48. page: 1,
  49. pageSize: 100 ,
  50. filters: JSON.stringify({ parentId: nodeId}),
  51. sortBy: 'id',
  52. sortOrder: 'ASC'
  53. }
  54. });
  55. if (res.status !== 200) throw new Error('获取子树失败');
  56. const response = await res.json();
  57. return response.data;
  58. },
  59. enabled: isExpanded && hasChildren,
  60. staleTime: 5 * 60 * 1000,
  61. gcTime: 10 * 60 * 1000,
  62. });
  63. if (isSubTreeLoading) {
  64. return (
  65. <div className="flex items-center justify-center py-2 px-3 text-muted-foreground">
  66. <Loader2 className="h-4 w-4 animate-spin mr-2" />
  67. 加载中...
  68. </div>
  69. );
  70. }
  71. if (!subTreeData) {
  72. return (
  73. <div className="py-2 px-3 text-muted-foreground text-sm">
  74. 暂无子节点
  75. </div>
  76. );
  77. }
  78. // subTreeData 是一个 AreaNode 数组,直接使用
  79. const childNodes = subTreeData || [];
  80. if (childNodes.length === 0) {
  81. return (
  82. <div className="py-2 px-3 text-muted-foreground text-sm">
  83. 暂无子节点
  84. </div>
  85. );
  86. }
  87. return (
  88. <div>
  89. {childNodes.map((node) => (
  90. <TreeNode
  91. key={node.id}
  92. node={node}
  93. depth={depth + 1}
  94. expandedNodes={expandedNodes}
  95. onToggleNode={onToggleNode}
  96. onEdit={onEdit}
  97. onDelete={onDelete}
  98. onToggleStatus={onToggleStatus}
  99. onAddChild={onAddChild}
  100. />
  101. ))}
  102. </div>
  103. );
  104. };
  105. // 树节点组件
  106. interface TreeNodeProps {
  107. node: AreaNode;
  108. depth?: number;
  109. expandedNodes: Set<number>;
  110. onToggleNode: (nodeId: number) => void;
  111. onEdit: (area: AreaNode) => void;
  112. onDelete: (area: AreaNode) => void;
  113. onToggleStatus: (area: AreaNode) => void;
  114. onAddChild: (area: AreaNode) => void;
  115. }
  116. const TreeNode: React.FC<TreeNodeProps> = ({
  117. node,
  118. depth = 0,
  119. expandedNodes,
  120. onToggleNode,
  121. onEdit,
  122. onDelete,
  123. onToggleStatus,
  124. onAddChild
  125. }) => {
  126. const isExpanded = expandedNodes.has(node.id);
  127. const isDisabled = node.isDisabled === 1;
  128. const hasChildren = node.level < 3; // 省级和市级节点可能有子节点
  129. return (
  130. <div key={node.id} className="select-none">
  131. {/* 节点行 */}
  132. <div
  133. className={cn(
  134. "group flex items-center gap-2 py-2 px-3 hover:bg-muted/50 cursor-pointer border-b",
  135. depth > 0 && "ml-6"
  136. )}
  137. style={{ marginLeft: `${depth * 24}px` }}
  138. >
  139. {/* 展开/收起按钮 */}
  140. {hasChildren && (
  141. <Button
  142. variant="ghost"
  143. size="sm"
  144. className="h-6 w-6 p-0"
  145. onClick={() => onToggleNode(node.id)}
  146. >
  147. {isExpanded ? (
  148. <ChevronDown className="h-4 w-4" />
  149. ) : (
  150. <ChevronRight className="h-4 w-4" />
  151. )}
  152. </Button>
  153. )}
  154. {!hasChildren && <div className="w-6" />}
  155. {/* 图标 */}
  156. <div className="flex-shrink-0">
  157. {hasChildren ? (
  158. isExpanded ? (
  159. <FolderOpen className="h-4 w-4 text-blue-500" />
  160. ) : (
  161. <Folder className="h-4 w-4 text-blue-400" />
  162. )
  163. ) : (
  164. <div className="h-4 w-4" />
  165. )}
  166. </div>
  167. {/* 节点信息 */}
  168. <div className="flex-1 flex items-center gap-3">
  169. <span className={cn("font-medium", isDisabled && "text-muted-foreground line-through")}>
  170. {node.name}
  171. </span>
  172. <Badge variant="outline" className="text-xs">
  173. {getLevelName(node.level)}
  174. </Badge>
  175. <span className="text-xs text-muted-foreground">
  176. {node.code}
  177. </span>
  178. <Badge variant={isDisabled ? "secondary" : "default"} className="text-xs">
  179. {isDisabled ? '禁用' : '启用'}
  180. </Badge>
  181. </div>
  182. {/* 操作按钮 */}
  183. <div className="flex gap-1 opacity-100 transition-opacity">
  184. {/* 新增子节点按钮 - 根据层级显示不同文本 */}
  185. {node.level < 3 && (
  186. <Button
  187. variant="outline"
  188. size="sm"
  189. onClick={(e) => {
  190. e.stopPropagation();
  191. onAddChild(node);
  192. }}
  193. >
  194. {node.level === 1 ? '新增市' : '新增区'}
  195. </Button>
  196. )}
  197. <Button
  198. variant="outline"
  199. size="sm"
  200. onClick={(e) => {
  201. e.stopPropagation();
  202. onEdit(node);
  203. }}
  204. >
  205. 编辑
  206. </Button>
  207. <Button
  208. variant="outline"
  209. size="sm"
  210. onClick={(e) => {
  211. e.stopPropagation();
  212. onToggleStatus(node);
  213. }}
  214. >
  215. {isDisabled ? '启用' : '禁用'}
  216. </Button>
  217. <Button
  218. variant="outline"
  219. size="sm"
  220. onClick={(e) => {
  221. e.stopPropagation();
  222. onDelete(node);
  223. }}
  224. >
  225. 删除
  226. </Button>
  227. </div>
  228. </div>
  229. {/* 子节点 */}
  230. {isExpanded && hasChildren && (
  231. <SubTreeLoader
  232. nodeId={node.id}
  233. isExpanded={isExpanded}
  234. hasChildren={hasChildren}
  235. depth={depth}
  236. expandedNodes={expandedNodes}
  237. onToggleNode={onToggleNode}
  238. onEdit={onEdit}
  239. onDelete={onDelete}
  240. onToggleStatus={onToggleStatus}
  241. onAddChild={onAddChild}
  242. />
  243. )}
  244. </div>
  245. );
  246. };
  247. export const AreaTreeAsync: React.FC<AreaTreeAsyncProps> = ({
  248. areas,
  249. expandedNodes,
  250. onToggleNode,
  251. onEdit,
  252. onDelete,
  253. onToggleStatus,
  254. onAddChild
  255. }) => {
  256. return (
  257. <div className="border rounded-lg bg-background">
  258. {areas.map(area => (
  259. <TreeNode
  260. key={area.id}
  261. node={area}
  262. depth={0}
  263. expandedNodes={expandedNodes}
  264. onToggleNode={onToggleNode}
  265. onEdit={onEdit}
  266. onDelete={onDelete}
  267. onToggleStatus={onToggleStatus}
  268. onAddChild={onAddChild}
  269. />
  270. ))}
  271. </div>
  272. );
  273. };
  274. // 获取层级显示名称
  275. const getLevelName = (level: number) => {
  276. switch (level) {
  277. case 1: return '省/直辖市';
  278. case 2: return '市';
  279. case 3: return '区/县';
  280. default: return '未知';
  281. }
  282. };