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 { 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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue