feat(mining-admin-web): 引荐关系改为树形可视化布局
- 仿照1.0 admin-web的树形结构 - 显示引荐人链(向上)、总部节点、当前用户 - 递归展开/收起直推下级 - 圆形+-按钮控制展开 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c141c3f6cd
commit
9a34e9d399
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue