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