rwadurian/frontend/mining-admin-web/src/features/users/components/referral-tree.tsx

279 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useCallback, useEffect } from 'react';
import Link from 'next/link';
import { useReferralTree } from '../hooks/use-users';
import { usersApi } from '../api/users.api';
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 type { ReferralNode } from '@/types/user';
import { cn } from '@/lib/utils';
interface ReferralTreeProps {
accountSequence: string;
}
export function ReferralTree({ accountSequence }: ReferralTreeProps) {
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
const [expandedNodes, setExpandedNodes] = useState<Record<string, ReferralNode[] | null>>({});
const { data: referralTree, isLoading } = useReferralTree(treeRootUser, 'both', 1);
// 当 referralTree 数据加载完成后,自动展开当前用户的直推下级
useEffect(() => {
if (referralTree?.currentUser && referralTree.directReferrals?.length > 0) {
setExpandedNodes((prev) => ({
...prev,
[referralTree.currentUser.accountSequence]: referralTree.directReferrals,
}));
}
}, [referralTree]);
// 切换推荐关系树的根节点
const handleTreeNodeClick = useCallback((node: ReferralNode) => {
setTreeRootUser(node.accountSequence);
}, []);
// 展开/收起节点的下级
const handleToggleNode = useCallback(async (nodeSeq: string, hasChildren: boolean) => {
if (!hasChildren) return;
// 如果已展开,则收起
if (expandedNodes[nodeSeq] !== undefined) {
setExpandedNodes((prev) => {
const newState = { ...prev };
delete newState[nodeSeq];
return newState;
});
return;
}
// 展开:先标记为 null加载中然后获取数据
setExpandedNodes((prev) => ({ ...prev, [nodeSeq]: null }));
try {
const treeData = await usersApi.getReferralTree(nodeSeq, 'down', 1);
setExpandedNodes((prev) => ({ ...prev, [nodeSeq]: treeData.directReferrals }));
} catch (error) {
console.error('获取下级失败:', error);
setExpandedNodes((prev) => {
const newState = { ...prev };
delete newState[nodeSeq];
return newState;
});
}
}, [expandedNodes]);
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
</CardContent>
</Card>
);
}
if (!referralTree) {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-center py-8"></p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg"></CardTitle>
{treeRootUser !== accountSequence && (
<Button variant="outline" size="sm" onClick={() => setTreeRootUser(accountSequence)}>
</Button>
)}
</CardHeader>
<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="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 TreeNodeCardProps {
node: ReferralNode;
isCurrentUser?: boolean;
isHighlight?: boolean;
onClick?: () => void;
}
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,
expandedNodes,
onToggle,
onClick,
}: 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="flex flex-col items-center">
{/* 节点内容 */}
<div className="flex flex-col items-center">
<TreeNodeCard
node={node}
isCurrentUser={isCurrentUser}
isHighlight={isHighlight}
onClick={() => onClick(node)}
/>
{/* 展开/收起按钮 */}
{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'
)}
onClick={(e) => {
e.stopPropagation();
onToggle(node.accountSequence, hasChildren);
}}
disabled={isLoading}
>
{isLoading ? '...' : isExpanded ? '' : '+'}
</button>
)}
</div>
{/* 递归渲染子节点 */}
{isExpanded && children.length > 0 && (
<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>
);
}