| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- import React from 'react';
- import { ChevronRight, ChevronDown, Folder, FolderOpen, Loader2 } from 'lucide-react';
- import { Button } from '@d8d/shared-ui-components/components/ui/button';
- import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
- import { cn } from '@d8d/shared-ui-components/utils';
- import { useQuery } from '@tanstack/react-query';
- import { areaClientManager } from '../api/areaClient';
- import type { AreaNode } from '../types/area';
- interface AreaTreeAsyncProps {
- areas: AreaNode[];
- expandedNodes: Set<number>;
- onToggleNode: (nodeId: number) => void;
- onEdit: (area: AreaNode) => void;
- onDelete: (area: AreaNode) => void;
- onToggleStatus: (area: AreaNode) => void;
- onAddChild: (area: AreaNode) => void;
- }
- // 子树加载组件
- interface SubTreeLoaderProps {
- nodeId: number;
- isExpanded: boolean;
- hasChildren: boolean;
- depth: number;
- expandedNodes: Set<number>;
- onToggleNode: (nodeId: number) => void;
- onEdit: (area: AreaNode) => void;
- onDelete: (area: AreaNode) => void;
- onToggleStatus: (area: AreaNode) => void;
- onAddChild: (area: AreaNode) => void;
- }
- const SubTreeLoader: React.FC<SubTreeLoaderProps> = ({
- nodeId,
- isExpanded,
- hasChildren,
- depth,
- expandedNodes,
- onToggleNode,
- onEdit,
- onDelete,
- onToggleStatus,
- onAddChild
- }) => {
- const { data: subTreeData, isLoading: isSubTreeLoading } = useQuery({
- queryKey: ['areas-subtree', nodeId],
- queryFn: async () => {
- const res = await areaClientManager.get().index.$get({
- query: {
- page: 1,
- pageSize: 100 ,
- filters: JSON.stringify({ parentId: nodeId}),
- sortBy: 'id',
- sortOrder: 'ASC'
- }
- });
- if (res.status !== 200) throw new Error('获取子树失败');
- const response = await res.json();
- return response.data;
- },
- enabled: isExpanded && hasChildren,
- staleTime: 5 * 60 * 1000,
- gcTime: 10 * 60 * 1000,
- });
- if (isSubTreeLoading) {
- return (
- <div className="flex items-center justify-center py-2 px-3 text-muted-foreground">
- <Loader2 className="h-4 w-4 animate-spin mr-2" />
- 加载中...
- </div>
- );
- }
- if (!subTreeData) {
- return (
- <div className="py-2 px-3 text-muted-foreground text-sm">
- 暂无子节点
- </div>
- );
- }
- // subTreeData 是一个 AreaNode 数组,直接使用
- const childNodes = subTreeData || [];
- if (childNodes.length === 0) {
- return (
- <div className="py-2 px-3 text-muted-foreground text-sm">
- 暂无子节点
- </div>
- );
- }
- return (
- <div>
- {childNodes.map((node) => (
- <TreeNode
- key={node.id}
- node={node}
- depth={depth + 1}
- expandedNodes={expandedNodes}
- onToggleNode={onToggleNode}
- onEdit={onEdit}
- onDelete={onDelete}
- onToggleStatus={onToggleStatus}
- onAddChild={onAddChild}
- />
- ))}
- </div>
- );
- };
- // 树节点组件
- interface TreeNodeProps {
- node: AreaNode;
- depth?: number;
- expandedNodes: Set<number>;
- onToggleNode: (nodeId: number) => void;
- onEdit: (area: AreaNode) => void;
- onDelete: (area: AreaNode) => void;
- onToggleStatus: (area: AreaNode) => void;
- onAddChild: (area: AreaNode) => void;
- }
- const TreeNode: React.FC<TreeNodeProps> = ({
- node,
- depth = 0,
- expandedNodes,
- onToggleNode,
- onEdit,
- onDelete,
- onToggleStatus,
- onAddChild
- }) => {
- const isExpanded = expandedNodes.has(node.id);
- const isDisabled = node.isDisabled === 1;
- const hasChildren = node.level < 3; // 省级和市级节点可能有子节点
- return (
- <div key={node.id} className="select-none">
- {/* 节点行 */}
- <div
- className={cn(
- "group flex items-center gap-2 py-2 px-3 hover:bg-muted/50 cursor-pointer border-b",
- depth > 0 && "ml-6"
- )}
- style={{ marginLeft: `${depth * 24}px` }}
- >
- {/* 展开/收起按钮 */}
- {hasChildren && (
- <Button
- variant="ghost"
- size="sm"
- className="h-6 w-6 p-0"
- onClick={() => onToggleNode(node.id)}
- >
- {isExpanded ? (
- <ChevronDown className="h-4 w-4" />
- ) : (
- <ChevronRight className="h-4 w-4" />
- )}
- </Button>
- )}
- {!hasChildren && <div className="w-6" />}
- {/* 图标 */}
- <div className="flex-shrink-0">
- {hasChildren ? (
- isExpanded ? (
- <FolderOpen className="h-4 w-4 text-blue-500" />
- ) : (
- <Folder className="h-4 w-4 text-blue-400" />
- )
- ) : (
- <div className="h-4 w-4" />
- )}
- </div>
- {/* 节点信息 */}
- <div className="flex-1 flex items-center gap-3">
- <span className={cn("font-medium", isDisabled && "text-muted-foreground line-through")}>
- {node.name}
- </span>
- <Badge variant="outline" className="text-xs">
- {getLevelName(node.level)}
- </Badge>
- <span className="text-xs text-muted-foreground">
- {node.code}
- </span>
- <Badge variant={isDisabled ? "secondary" : "default"} className="text-xs">
- {isDisabled ? '禁用' : '启用'}
- </Badge>
- </div>
- {/* 操作按钮 */}
- <div className="flex gap-1 opacity-100 transition-opacity">
- {/* 新增子节点按钮 - 根据层级显示不同文本 */}
- {node.level < 3 && (
- <Button
- variant="outline"
- size="sm"
- onClick={(e) => {
- e.stopPropagation();
- onAddChild(node);
- }}
- >
- {node.level === 1 ? '新增市' : '新增区'}
- </Button>
- )}
- <Button
- variant="outline"
- size="sm"
- onClick={(e) => {
- e.stopPropagation();
- onEdit(node);
- }}
- >
- 编辑
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={(e) => {
- e.stopPropagation();
- onToggleStatus(node);
- }}
- >
- {isDisabled ? '启用' : '禁用'}
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={(e) => {
- e.stopPropagation();
- onDelete(node);
- }}
- >
- 删除
- </Button>
- </div>
- </div>
- {/* 子节点 */}
- {isExpanded && hasChildren && (
- <SubTreeLoader
- nodeId={node.id}
- isExpanded={isExpanded}
- hasChildren={hasChildren}
- depth={depth}
- expandedNodes={expandedNodes}
- onToggleNode={onToggleNode}
- onEdit={onEdit}
- onDelete={onDelete}
- onToggleStatus={onToggleStatus}
- onAddChild={onAddChild}
- />
- )}
- </div>
- );
- };
- export const AreaTreeAsync: React.FC<AreaTreeAsyncProps> = ({
- areas,
- expandedNodes,
- onToggleNode,
- onEdit,
- onDelete,
- onToggleStatus,
- onAddChild
- }) => {
- return (
- <div className="border rounded-lg bg-background">
- {areas.map(area => (
- <TreeNode
- key={area.id}
- node={area}
- depth={0}
- expandedNodes={expandedNodes}
- onToggleNode={onToggleNode}
- onEdit={onEdit}
- onDelete={onDelete}
- onToggleStatus={onToggleStatus}
- onAddChild={onAddChild}
- />
- ))}
- </div>
- );
- };
- // 获取层级显示名称
- const getLevelName = (level: number) => {
- switch (level) {
- case 1: return '省/直辖市';
- case 2: return '市';
- case 3: return '区/县';
- default: return '未知';
- }
- };
|