AreaTreeAsync.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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: AreaNode) => (
  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 < 4; // 省级、市级和区县级节点可能有子节点
  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 < 4 && (
  186. <Button
  187. variant="outline"
  188. size="sm"
  189. onClick={(e) => {
  190. e.stopPropagation();
  191. onAddChild(node);
  192. }}
  193. >
  194. {node.level === 1 ? '新增市' :
  195. node.level === 2 ? '新增区' : '新增乡镇'}
  196. </Button>
  197. )}
  198. <Button
  199. variant="outline"
  200. size="sm"
  201. onClick={(e) => {
  202. e.stopPropagation();
  203. onEdit(node);
  204. }}
  205. >
  206. 编辑
  207. </Button>
  208. <Button
  209. variant="outline"
  210. size="sm"
  211. onClick={(e) => {
  212. e.stopPropagation();
  213. onToggleStatus(node);
  214. }}
  215. >
  216. {isDisabled ? '启用' : '禁用'}
  217. </Button>
  218. <Button
  219. variant="outline"
  220. size="sm"
  221. onClick={(e) => {
  222. e.stopPropagation();
  223. onDelete(node);
  224. }}
  225. >
  226. 删除
  227. </Button>
  228. </div>
  229. </div>
  230. {/* 子节点 */}
  231. {isExpanded && hasChildren && (
  232. <SubTreeLoader
  233. nodeId={node.id}
  234. isExpanded={isExpanded}
  235. hasChildren={hasChildren}
  236. depth={depth}
  237. expandedNodes={expandedNodes}
  238. onToggleNode={onToggleNode}
  239. onEdit={onEdit}
  240. onDelete={onDelete}
  241. onToggleStatus={onToggleStatus}
  242. onAddChild={onAddChild}
  243. />
  244. )}
  245. </div>
  246. );
  247. };
  248. export const AreaTreeAsync: React.FC<AreaTreeAsyncProps> = ({
  249. areas,
  250. expandedNodes,
  251. onToggleNode,
  252. onEdit,
  253. onDelete,
  254. onToggleStatus,
  255. onAddChild
  256. }) => {
  257. return (
  258. <div className="border rounded-lg bg-background">
  259. {areas.map(area => (
  260. <TreeNode
  261. key={area.id}
  262. node={area}
  263. depth={0}
  264. expandedNodes={expandedNodes}
  265. onToggleNode={onToggleNode}
  266. onEdit={onEdit}
  267. onDelete={onDelete}
  268. onToggleStatus={onToggleStatus}
  269. onAddChild={onAddChild}
  270. />
  271. ))}
  272. </div>
  273. );
  274. };
  275. // 获取层级显示名称
  276. const getLevelName = (level: number) => {
  277. switch (level) {
  278. case 1: return '省/直辖市';
  279. case 2: return '市';
  280. case 3: return '区/县';
  281. case 4: return '街道/乡镇';
  282. default: return '未知';
  283. }
  284. };