279 lines
9.3 KiB
TypeScript
279 lines
9.3 KiB
TypeScript
'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>
|
||
);
|
||
}
|