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,
|
useWalletLedger,
|
||||||
useAuthorizationDetail,
|
useAuthorizationDetail,
|
||||||
} from '@/hooks/useUserDetailPage';
|
} from '@/hooks/useUserDetailPage';
|
||||||
|
import { userDetailService } from '@/services/userDetailService';
|
||||||
import type {
|
import type {
|
||||||
ReferralNode,
|
ReferralNode,
|
||||||
PlantingLedgerItem,
|
PlantingLedgerItem,
|
||||||
|
|
@ -139,7 +140,8 @@ export default function UserDetailPage() {
|
||||||
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
|
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
|
||||||
const [plantingPage, setPlantingPage] = useState(1);
|
const [plantingPage, setPlantingPage] = useState(1);
|
||||||
const [walletPage, setWalletPage] = 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);
|
const { data: userDetail, isLoading: detailLoading, error: detailError } = useUserFullDetail(accountSequence);
|
||||||
|
|
@ -167,6 +169,37 @@ export default function UserDetailPage() {
|
||||||
setTreeRootUser(node.accountSequence);
|
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(() => {
|
const handleBack = useCallback(() => {
|
||||||
router.push('/users');
|
router.push('/users');
|
||||||
|
|
@ -370,72 +403,18 @@ export default function UserDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 当前用户 */}
|
{/* 当前用户及其递归下级 */}
|
||||||
<div className={styles.referralTree__current}>
|
<div className={styles.referralTree__currentWrapper}>
|
||||||
<div
|
<ReferralNodeItem
|
||||||
className={cn(
|
node={referralTree.currentUser}
|
||||||
styles.referralTree__node,
|
isCurrentUser={true}
|
||||||
styles['referralTree__node--current'],
|
isHighlight={referralTree.currentUser.accountSequence === accountSequence}
|
||||||
referralTree.currentUser.accountSequence === accountSequence &&
|
expandedNodes={expandedNodes}
|
||||||
styles['referralTree__node--highlight']
|
onToggle={handleToggleNode}
|
||||||
)}
|
onClick={handleTreeNodeClick}
|
||||||
>
|
/>
|
||||||
<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>
|
</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 && (
|
{referralTree.directReferrals.length === 0 && referralTree.ancestors.length === 0 && (
|
||||||
<div className={styles.referralTree__empty}>暂无引荐关系</div>
|
<div className={styles.referralTree__empty}>暂无引荐关系</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -810,3 +789,87 @@ export default function UserDetailPage() {
|
||||||
</PageContainer>
|
</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;
|
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 {
|
.referralTree__directReferrals {
|
||||||
@include flex-column;
|
@include flex-column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue