feat(mining-admin-web): 引荐关系改为树形可视化布局

- 仿照1.0 admin-web的树形结构
- 显示引荐人链(向上)、总部节点、当前用户
- 递归展开/收起直推下级
- 圆形+-按钮控制展开

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-12 03:55:51 -08:00
parent c141c3f6cd
commit 9a34e9d399
1 changed files with 131 additions and 142 deletions

View File

@ -8,8 +8,6 @@ import { formatNumber } from '@/lib/utils/format';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { ChevronDown, ChevronRight, Users, TreePine, Building2 } from 'lucide-react';
import type { ReferralNode } from '@/types/user'; import type { ReferralNode } from '@/types/user';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -101,187 +99,178 @@ export function ReferralTree({ accountSequence }: ReferralTreeProps) {
return ( return (
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
{treeRootUser !== accountSequence && ( {treeRootUser !== accountSequence && (
<Button variant="outline" size="sm" onClick={() => setTreeRootUser(accountSequence)}> <Button variant="outline" size="sm" onClick={() => setTreeRootUser(accountSequence)}>
</Button> </Button>
)} )}
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent>
{/* 向上的引荐人链 */} {/* 树形结构容器 - 仿照1.0 admin-web的样式 */}
<div className="space-y-2"> <div className="flex flex-col items-center gap-4 p-6 bg-muted/30 rounded-lg">
<p className="text-sm font-medium text-muted-foreground"> ()</p> {/* 向上的引荐人链 */}
{(referralTree.ancestors?.length ?? 0) > 0 ? ( <div className="flex flex-col items-center gap-2">
<div className="space-y-2"> <span className="text-xs text-muted-foreground uppercase tracking-wide mb-2">
{(referralTree.ancestors || []).map((ancestor, index) => ( ()
<div key={ancestor.accountSequence}> </span>
<ReferralNodeCard {(referralTree.ancestors?.length ?? 0) > 0 ? (
node={ancestor} <>
onClick={() => handleTreeNodeClick(ancestor)} <div className="flex flex-col items-center gap-2">
variant="ancestor" {(referralTree.ancestors || []).map((ancestor, index) => (
/> <div key={ancestor.accountSequence} className="flex flex-col items-center">
{index < (referralTree.ancestors?.length ?? 0) - 1 && ( <TreeNodeCard
<div className="flex justify-center py-1"> node={ancestor}
<ChevronDown className="h-4 w-4 text-muted-foreground" /> onClick={() => handleTreeNodeClick(ancestor)}
/>
{index < (referralTree.ancestors?.length ?? 0) - 1 && (
<div className="text-muted-foreground text-lg py-1"></div>
)}
</div> </div>
)} ))}
</div> </div>
))} <div className="text-muted-foreground text-lg py-1"></div>
<div className="flex justify-center py-1"> </>
<ChevronDown className="h-4 w-4 text-muted-foreground" /> ) : (
</div> <>
</div> {/* 总部节点 */}
) : ( <div className="flex items-center justify-center px-6 py-2 bg-gradient-to-r from-primary/10 to-green-500/10 border-2 border-primary rounded-lg min-w-[100px]">
<div className="flex items-center justify-center gap-2 p-3 bg-muted rounded-lg"> <span className="text-base font-bold text-primary"></span>
<Building2 className="h-4 w-4" /> </div>
<span className="text-sm font-medium"></span> <div className="text-muted-foreground text-lg py-1"></div>
</div> </>
)} )}
</div>
{/* 当前用户 */}
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground"></p>
<ReferralNodeCard
node={referralTree.currentUser}
isCurrentUser
isHighlight={referralTree.currentUser.accountSequence === accountSequence}
expandedNodes={expandedNodes}
onToggle={handleToggleNode}
onClick={handleTreeNodeClick}
/>
</div>
{/* 直推下级列表 */}
{(referralTree.directReferrals?.length ?? 0) > 0 && (
<div className="space-y-2 ml-6 border-l-2 border-muted pl-4">
<p className="text-sm font-medium text-muted-foreground">
({referralTree.directReferrals?.length ?? 0})
</p>
<div className="space-y-2">
{(referralTree.directReferrals || []).map((child) => (
<ReferralNodeCard
key={child.accountSequence}
node={child}
expandedNodes={expandedNodes}
onToggle={handleToggleNode}
onClick={handleTreeNodeClick}
showExpandButton
/>
))}
</div>
</div> </div>
)}
{/* 当前用户节点 - 递归组件 */}
<div className="flex flex-col items-center w-full">
<ReferralNodeItem
node={referralTree.currentUser}
isCurrentUser={true}
isHighlight={referralTree.currentUser.accountSequence === accountSequence}
expandedNodes={expandedNodes}
onToggle={handleToggleNode}
onClick={handleTreeNodeClick}
/>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
); );
} }
interface ReferralNodeCardProps { /**
* -
*/
interface TreeNodeCardProps {
node: ReferralNode; node: ReferralNode;
isCurrentUser?: boolean; isCurrentUser?: boolean;
isHighlight?: boolean; isHighlight?: boolean;
variant?: 'ancestor' | 'current' | 'child'; onClick?: () => void;
expandedNodes?: Record<string, ReferralNode[] | null>;
onToggle?: (nodeSeq: string, hasChildren: boolean) => void;
onClick?: (node: ReferralNode) => void;
showExpandButton?: boolean;
} }
function ReferralNodeCard({ function TreeNodeCard({ node, isCurrentUser, isHighlight, onClick }: TreeNodeCardProps) {
return (
<button
type="button"
className={cn(
'flex flex-col items-center px-6 py-3 bg-background border-2 border-border rounded-lg cursor-pointer transition-all',
'hover:border-primary hover:shadow-md hover:-translate-y-0.5',
isCurrentUser && 'border-primary bg-primary/5',
isHighlight && 'border-[3px] ring-4 ring-primary/20'
)}
onClick={onClick}
>
<span className="font-mono text-base font-bold text-foreground">
{node.accountSequence}
</span>
<span className="text-xs text-muted-foreground mt-1">
{node.nickname || '未设置'}
</span>
<span className="text-xs text-muted-foreground mt-1">
: {formatNumber(node.personalAdoptions)} / : {formatNumber(node.teamAdoptions)}
</span>
{node.directReferralCount > 0 && (
<span className="text-xs text-primary font-medium mt-1">
: {formatNumber(node.directReferralCount)}
</span>
)}
</button>
);
}
/**
* - /
*/
interface ReferralNodeItemProps {
node: ReferralNode;
isCurrentUser?: boolean;
isHighlight?: boolean;
expandedNodes: Record<string, ReferralNode[] | null>;
onToggle: (nodeSeq: string, hasChildren: boolean) => void;
onClick: (node: ReferralNode) => void;
}
function ReferralNodeItem({
node, node,
isCurrentUser = false, isCurrentUser = false,
isHighlight = false, isHighlight = false,
variant, expandedNodes,
expandedNodes = {},
onToggle, onToggle,
onClick, onClick,
showExpandButton = false, }: ReferralNodeItemProps) {
}: ReferralNodeCardProps) {
const hasChildren = node.directReferralCount > 0; const hasChildren = node.directReferralCount > 0;
const isExpanded = expandedNodes[node.accountSequence] !== undefined; const isExpanded = expandedNodes[node.accountSequence] !== undefined;
const isLoading = expandedNodes[node.accountSequence] === null; const isLoading = expandedNodes[node.accountSequence] === null;
const children = expandedNodes[node.accountSequence] || []; const children = expandedNodes[node.accountSequence] || [];
return ( return (
<div className="space-y-2"> <div className="flex flex-col items-center">
<div {/* 节点内容 */}
className={cn( <div className="flex flex-col items-center">
'flex items-center gap-3 p-3 rounded-lg border transition-colors cursor-pointer hover:bg-accent', <TreeNodeCard
isCurrentUser && 'border-primary bg-primary/5', node={node}
isHighlight && 'ring-2 ring-primary' isCurrentUser={isCurrentUser}
)} isHighlight={isHighlight}
onClick={() => onClick?.(node)} onClick={() => onClick(node)}
> />
<Avatar className="h-10 w-10">
<AvatarImage src={node.avatar || undefined} alt={node.nickname || ''} />
<AvatarFallback>{node.nickname?.charAt(0) || 'U'}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0"> {/* 展开/收起按钮 */}
<div className="flex items-center gap-2"> {hasChildren && (
<span className="font-mono text-sm">{node.accountSequence}</span> <button
<span className="text-sm truncate">{node.nickname || '未设置'}</span> type="button"
</div> className={cn(
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-1"> 'flex items-center justify-center w-7 h-7 mt-2 bg-primary text-white',
<span className="flex items-center gap-1"> 'border-none rounded-full cursor-pointer text-lg font-bold leading-none',
<TreePine className="h-3 w-3 text-green-600" /> 'transition-all hover:opacity-80 hover:scale-110',
: {formatNumber(node.personalAdoptions)} 'disabled:opacity-50 disabled:cursor-not-allowed'
</span>
<span className="flex items-center gap-1">
<Users className="h-3 w-3 text-blue-600" />
: {formatNumber(node.teamAdoptions)}
</span>
{node.directReferralCount > 0 && (
<span>: {formatNumber(node.directReferralCount)}</span>
)} )}
</div>
</div>
{showExpandButton && hasChildren && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggle?.(node.accountSequence, hasChildren); onToggle(node.accountSequence, hasChildren);
}} }}
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? ( {isLoading ? '...' : isExpanded ? '' : '+'}
<span className="animate-spin">...</span> </button>
) : isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
)} )}
<Link
href={`/users/${node.accountSequence}`}
className="text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
</Link>
</div> </div>
{/* 展开的子节点 */} {/* 递归渲染子节点 */}
{isExpanded && children.length > 0 && ( {isExpanded && children.length > 0 && (
<div className="ml-6 border-l-2 border-muted pl-4 space-y-2"> <div className="flex flex-col items-center mt-2 w-full">
{children.map((child) => ( <div className="text-muted-foreground text-lg py-1"></div>
<ReferralNodeCard <div className="flex flex-wrap gap-4 justify-center mt-2">
key={child.accountSequence} {children.map((child) => (
node={child} <ReferralNodeItem
expandedNodes={expandedNodes} key={child.accountSequence}
onToggle={onToggle} node={child}
onClick={onClick} expandedNodes={expandedNodes}
showExpandButton onToggle={onToggle}
/> onClick={onClick}
))} />
))}
</div>
</div> </div>
)} )}
</div> </div>