feat(admin-web): 引荐关系树支持递归展开每个节点

- 创建 ReferralNodeItem 递归组件
- 每个有下级的节点都显示"+"按钮
- 点击"+"异步加载并展开该节点的下级
- 展开后按钮变为"-",点击收起
- 加载中显示"..."
- 子节点也支持递归展开,可无限层级

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-07 22:24:21 -08:00
parent fff386c000
commit 3ed72499a0
2 changed files with 162 additions and 65 deletions

View File

@ -15,6 +15,7 @@ import {
useWalletLedger,
useAuthorizationDetail,
} from '@/hooks/useUserDetailPage';
import { userDetailService } from '@/services/userDetailService';
import type {
ReferralNode,
PlantingLedgerItem,
@ -139,7 +140,8 @@ export default function UserDetailPage() {
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
const [plantingPage, setPlantingPage] = useState(1);
const [walletPage, setWalletPage] = useState(1);
const [showDirectReferrals, setShowDirectReferrals] = useState(false); // 默认收起直推下级
// 存储已展开节点的状态key 是节点 accountSequencevalue 是其子节点数组null 表示未加载)
const [expandedNodes, setExpandedNodes] = useState<Record<string, ReferralNode[] | null>>({});
// 获取用户完整信息
const { data: userDetail, isLoading: detailLoading, error: detailError } = useUserFullDetail(accountSequence);
@ -167,6 +169,37 @@ export default function UserDetailPage() {
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 userDetailService.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]);
// 返回列表
const handleBack = useCallback(() => {
router.push('/users');
@ -370,72 +403,18 @@ export default function UserDetailPage() {
</div>
)}
{/* 当前用户 */}
<div className={styles.referralTree__current}>
<div
className={cn(
styles.referralTree__node,
styles['referralTree__node--current'],
referralTree.currentUser.accountSequence === accountSequence &&
styles['referralTree__node--highlight']
)}
>
<span className={styles.referralTree__nodeSeq}>
{referralTree.currentUser.accountSequence}
</span>
<span className={styles.referralTree__nodeNickname}>
{referralTree.currentUser.nickname || '未设置'}
</span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(referralTree.currentUser.personalAdoptions)}
</span>
<span className={styles.referralTree__nodeCount}>
: {formatNumber(referralTree.currentUser.directReferralCount)}
</span>
</div>
{/* 展开/收起直推下级按钮 */}
{referralTree.directReferrals.length > 0 && (
<button
className={styles.referralTree__toggleButton}
onClick={() => setShowDirectReferrals(!showDirectReferrals)}
>
{showDirectReferrals ? '' : '+'}
</button>
)}
{/* 当前用户及其递归下级 */}
<div className={styles.referralTree__currentWrapper}>
<ReferralNodeItem
node={referralTree.currentUser}
isCurrentUser={true}
isHighlight={referralTree.currentUser.accountSequence === accountSequence}
expandedNodes={expandedNodes}
onToggle={handleToggleNode}
onClick={handleTreeNodeClick}
/>
</div>
{/* 引荐用户 - 默认收起,点击展开 */}
{referralTree.directReferrals.length > 0 && showDirectReferrals && (
<div className={styles.referralTree__directReferrals}>
<div className={styles.referralTree__connector}></div>
<div className={styles.referralTree__label}>
({referralTree.directReferrals.length})
</div>
<div className={styles.referralTree__nodeGrid}>
{referralTree.directReferrals.map((referral) => (
<button
key={referral.accountSequence}
className={styles.referralTree__node}
onClick={() => handleTreeNodeClick(referral)}
>
<span className={styles.referralTree__nodeSeq}>{referral.accountSequence}</span>
<span className={styles.referralTree__nodeNickname}>
{referral.nickname || '未设置'}
</span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(referral.personalAdoptions)}
</span>
{referral.directReferralCount > 0 && (
<span className={styles.referralTree__nodeCount}>
: {formatNumber(referral.directReferralCount)}
</span>
)}
</button>
))}
</div>
</div>
)}
{referralTree.directReferrals.length === 0 && referralTree.ancestors.length === 0 && (
<div className={styles.referralTree__empty}></div>
)}
@ -810,3 +789,87 @@ export default function UserDetailPage() {
</PageContainer>
);
}
/**
*
*/
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={styles.referralTree__nodeItem}>
<div className={styles.referralTree__nodeContent}>
<button
className={cn(
styles.referralTree__node,
isCurrentUser && styles['referralTree__node--current'],
isHighlight && styles['referralTree__node--highlight']
)}
onClick={() => onClick(node)}
>
<span className={styles.referralTree__nodeSeq}>{node.accountSequence}</span>
<span className={styles.referralTree__nodeNickname}>
{node.nickname || '未设置'}
</span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(node.personalAdoptions)}
</span>
{node.directReferralCount > 0 && (
<span className={styles.referralTree__nodeCount}>
: {formatNumber(node.directReferralCount)}
</span>
)}
</button>
{/* 展开/收起按钮 */}
{hasChildren && (
<button
className={styles.referralTree__toggleButton}
onClick={(e) => {
e.stopPropagation();
onToggle(node.accountSequence, hasChildren);
}}
disabled={isLoading}
>
{isLoading ? '...' : isExpanded ? '' : '+'}
</button>
)}
</div>
{/* 递归渲染子节点 */}
{isExpanded && children.length > 0 && (
<div className={styles.referralTree__children}>
<div className={styles.referralTree__connector}></div>
<div className={styles.referralTree__childrenGrid}>
{children.map((child) => (
<ReferralNodeItem
key={child.accountSequence}
node={child}
expandedNodes={expandedNodes}
onToggle={onToggle}
onClick={onClick}
/>
))}
</div>
</div>
)}
</div>
);
}

View File

@ -425,6 +425,40 @@
font-weight: $font-weight-medium;
}
// 递归节点组件样式
.referralTree__currentWrapper {
@include flex-column;
align-items: center;
width: 100%;
}
.referralTree__nodeItem {
@include flex-column;
align-items: center;
}
.referralTree__nodeContent {
@include flex-column;
align-items: center;
}
.referralTree__children {
@include flex-column;
align-items: center;
margin-top: $spacing-sm;
padding-left: $spacing-xl;
border-left: 2px dashed $border-color;
width: 100%;
}
.referralTree__childrenGrid {
display: flex;
flex-wrap: wrap;
gap: $spacing-md;
justify-content: center;
margin-top: $spacing-sm;
}
.referralTree__directReferrals {
@include flex-column;
align-items: center;