GoodsCategoryTreeAsync.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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 { goodsCategoryClientManager } from '../api/goodsCategoryClient';
  8. import type { GoodsCategoryNode } from '../types/goodsCategory';
  9. interface GoodsCategoryTreeAsyncProps {
  10. categories: GoodsCategoryNode[];
  11. expandedNodes: Set<number>;
  12. onToggleNode: (nodeId: number) => void;
  13. onEdit: (category: GoodsCategoryNode) => void;
  14. onDelete: (category: GoodsCategoryNode) => void;
  15. onToggleStatus: (category: GoodsCategoryNode) => void;
  16. onAddChild: (category: GoodsCategoryNode) => 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: (category: GoodsCategoryNode) => void;
  27. onDelete: (category: GoodsCategoryNode) => void;
  28. onToggleStatus: (category: GoodsCategoryNode) => void;
  29. onAddChild: (category: GoodsCategoryNode) => 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: ['goods-categories-subtree', nodeId],
  45. queryFn: async () => {
  46. const res = await goodsCategoryClientManager.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 是一个 GoodsCategoryNode 数组,直接使用
  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: GoodsCategoryNode) => (
  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: GoodsCategoryNode;
  108. depth?: number;
  109. expandedNodes: Set<number>;
  110. onToggleNode: (nodeId: number) => void;
  111. onEdit: (category: GoodsCategoryNode) => void;
  112. onDelete: (category: GoodsCategoryNode) => void;
  113. onToggleStatus: (category: GoodsCategoryNode) => void;
  114. onAddChild: (category: GoodsCategoryNode) => 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.state === 2;
  128. const hasChildren = true; // 商品分类理论上可以无限层级
  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. {node.imageFile?.fullUrl && (
  176. <img
  177. src={node.imageFile.fullUrl}
  178. alt={node.name}
  179. className="w-8 h-8 object-cover rounded"
  180. onError={(e) => {
  181. e.currentTarget.src = '/placeholder.png';
  182. }}
  183. />
  184. )}
  185. <Badge variant={isDisabled ? "secondary" : "default"} className="text-xs">
  186. {isDisabled ? '禁用' : '启用'}
  187. </Badge>
  188. </div>
  189. {/* 操作按钮 */}
  190. <div className="flex gap-1 opacity-100 transition-opacity">
  191. {/* 新增子节点按钮 */}
  192. <Button
  193. variant="outline"
  194. size="sm"
  195. onClick={(e) => {
  196. e.stopPropagation();
  197. onAddChild(node);
  198. }}
  199. >
  200. 新增子分类
  201. </Button>
  202. <Button
  203. variant="outline"
  204. size="sm"
  205. onClick={(e) => {
  206. e.stopPropagation();
  207. onEdit(node);
  208. }}
  209. >
  210. 编辑
  211. </Button>
  212. <Button
  213. variant="outline"
  214. size="sm"
  215. onClick={(e) => {
  216. e.stopPropagation();
  217. onToggleStatus(node);
  218. }}
  219. >
  220. {isDisabled ? '启用' : '禁用'}
  221. </Button>
  222. <Button
  223. variant="outline"
  224. size="sm"
  225. onClick={(e) => {
  226. e.stopPropagation();
  227. onDelete(node);
  228. }}
  229. >
  230. 删除
  231. </Button>
  232. </div>
  233. </div>
  234. {/* 子节点 */}
  235. {isExpanded && hasChildren && (
  236. <SubTreeLoader
  237. nodeId={node.id}
  238. isExpanded={isExpanded}
  239. hasChildren={hasChildren}
  240. depth={depth}
  241. expandedNodes={expandedNodes}
  242. onToggleNode={onToggleNode}
  243. onEdit={onEdit}
  244. onDelete={onDelete}
  245. onToggleStatus={onToggleStatus}
  246. onAddChild={onAddChild}
  247. />
  248. )}
  249. </div>
  250. );
  251. };
  252. export const GoodsCategoryTreeAsync: React.FC<GoodsCategoryTreeAsyncProps> = ({
  253. categories,
  254. expandedNodes,
  255. onToggleNode,
  256. onEdit,
  257. onDelete,
  258. onToggleStatus,
  259. onAddChild
  260. }) => {
  261. return (
  262. <div className="border rounded-lg bg-background">
  263. {categories.map(category => (
  264. <TreeNode
  265. key={category.id}
  266. node={category}
  267. depth={0}
  268. expandedNodes={expandedNodes}
  269. onToggleNode={onToggleNode}
  270. onEdit={onEdit}
  271. onDelete={onDelete}
  272. onToggleStatus={onToggleStatus}
  273. onAddChild={onAddChild}
  274. />
  275. ))}
  276. </div>
  277. );
  278. };
  279. // 获取层级显示名称
  280. const getLevelName = (level: number) => {
  281. switch (level) {
  282. case 0: return '顶级分类';
  283. case 1: return '一级分类';
  284. case 2: return '二级分类';
  285. case 3: return '三级分类';
  286. case 4: return '四级分类';
  287. default: return `${level}级分类`;
  288. }
  289. };