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:
parent
fff386c000
commit
3ed72499a0
|
|
@ -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 是节点 accountSequence,value 是其子节点数组(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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue