1167 lines
53 KiB
TypeScript
1167 lines
53 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useCallback, useEffect } from 'react';
|
||
import { useParams, useRouter } from 'next/navigation';
|
||
import Link from 'next/link';
|
||
import { Button } from '@/components/common';
|
||
import { PageContainer } from '@/components/layout';
|
||
import { cn } from '@/utils/helpers';
|
||
import { formatNumber, formatRanking } from '@/utils/formatters';
|
||
import {
|
||
useUserFullDetail,
|
||
useReferralTree,
|
||
usePlantingLedger,
|
||
useWalletLedger,
|
||
useAuthorizationDetail,
|
||
} from '@/hooks/useUserDetailPage';
|
||
import { userDetailService } from '@/services/userDetailService';
|
||
// [2026-02-05] 新增:合同服务
|
||
import { contractService, CONTRACT_STATUS_LABELS, type ContractsListResponse } from '@/services/contractService';
|
||
|
||
// 获取合同状态颜色(内联样式)
|
||
const getContractStatusStyle = (status: string): React.CSSProperties => {
|
||
switch (status) {
|
||
case 'SIGNED':
|
||
return { color: '#16a34a' };
|
||
case 'PENDING':
|
||
case 'SCROLLED':
|
||
case 'ACKNOWLEDGED':
|
||
return { color: '#ca8a04' };
|
||
case 'EXPIRED':
|
||
case 'TIMEOUT':
|
||
return { color: '#dc2626' };
|
||
default:
|
||
return { color: '#4b5563' };
|
||
}
|
||
};
|
||
import type { ReferralNode } from '@/types/userDetail.types';
|
||
import { PROVINCE_CODE_NAMES, CITY_CODE_NAMES } from '@/types';
|
||
import styles from './user-detail.module.scss';
|
||
|
||
// Tab 类型
|
||
// [2026-02-05] 更新:新增 contracts Tab
|
||
type TabType = 'referral' | 'planting' | 'wallet' | 'authorization' | 'contracts';
|
||
|
||
const tabs: { key: TabType; label: string }[] = [
|
||
{ key: 'referral', label: '引荐关系' },
|
||
{ key: 'planting', label: '认种信息' },
|
||
{ key: 'wallet', label: '钱包信息' },
|
||
{ key: 'authorization', label: '授权信息' },
|
||
// [2026-02-05] 新增:合同信息 Tab
|
||
{ key: 'contracts', label: '合同信息' },
|
||
];
|
||
|
||
// 流水类型标签
|
||
const entryTypeLabels: Record<string, string> = {
|
||
DEPOSIT: '充值',
|
||
DEPOSIT_USDT: '绿积分充值',
|
||
DEPOSIT_BNB: 'BNB充值',
|
||
WITHDRAW: '提现',
|
||
WITHDRAW_FROZEN: '提现冻结',
|
||
WITHDRAW_CONFIRMED: '提现确认',
|
||
WITHDRAW_CANCELLED: '提现取消',
|
||
PLANTING_PAYMENT: '认种支付',
|
||
PLANTING_FROZEN: '认种冻结',
|
||
PLANTING_DEDUCT: '认种扣款',
|
||
REWARD_PENDING: '收益待领取',
|
||
REWARD_SETTLED: '收益结算',
|
||
REWARD_EXPIRED: '收益过期',
|
||
TRANSFER_OUT: '转出',
|
||
TRANSFER_IN: '转入',
|
||
INTERNAL_TRANSFER: '内部转账',
|
||
ADMIN_ADJUSTMENT: '管理员调整',
|
||
SYSTEM_DEDUCT: '系统扣款',
|
||
FEE: '手续费',
|
||
};
|
||
|
||
const assetTypeLabels: Record<string, string> = {
|
||
USDT: '绿积分',
|
||
DST: 'DST',
|
||
BNB: 'BNB',
|
||
OG: 'OG',
|
||
RWAD: 'RWAD',
|
||
HASHPOWER: '算力',
|
||
};
|
||
|
||
const plantingStatusLabels: Record<string, string> = {
|
||
CREATED: '已创建',
|
||
PAID: '已支付',
|
||
FUND_ALLOCATED: '资金已分配',
|
||
MINING_ENABLED: '已开始挖矿',
|
||
CANCELLED: '已取消',
|
||
EXPIRED: '已过期',
|
||
};
|
||
|
||
const roleTypeLabels: Record<string, string> = {
|
||
COMMUNITY: '部门权益',
|
||
COMMUNITY_PARTNER: '部门权益',
|
||
AUTH_PROVINCE_COMPANY: '省团队', // 授权省公司 = 省团队权益
|
||
PROVINCE_COMPANY: '省区域', // 正式省公司 = 省区域权益
|
||
AUTH_CITY_COMPANY: '市团队', // 授权市公司 = 市团队权益(40U)
|
||
CITY_COMPANY: '市区域', // 正式市公司 = 市区域权益(35U+2%算力)
|
||
};
|
||
|
||
// 判断是否为社区/部门类型角色
|
||
const isCommunityRole = (roleType: string): boolean => {
|
||
return roleType === 'COMMUNITY' || roleType === 'COMMUNITY_PARTNER';
|
||
};
|
||
|
||
// 判断是否为市级角色
|
||
const isCityRole = (roleType: string): boolean => {
|
||
return roleType === 'AUTH_CITY_COMPANY' || roleType === 'CITY_COMPANY';
|
||
};
|
||
|
||
// 获取区域标签名称
|
||
const getRegionLabel = (roleType: string): string => {
|
||
if (isCommunityRole(roleType)) return '部门名称:';
|
||
if (isCityRole(roleType)) return '城市名称:';
|
||
return '区域:';
|
||
};
|
||
|
||
const authStatusLabels: Record<string, string> = {
|
||
PENDING: '待授权',
|
||
AUTHORIZED: '已授权',
|
||
REVOKED: '已撤销',
|
||
EXPIRED: '已过期',
|
||
};
|
||
|
||
const assessmentResultLabels: Record<string, string> = {
|
||
NOT_ASSESSED: '未考核',
|
||
PASSED: '通过',
|
||
FAILED: '未通过',
|
||
BYPASSED: '豁免',
|
||
};
|
||
|
||
// 权益操作类型标签
|
||
const benefitActionLabels: Record<string, string> = {
|
||
ACTIVATED: '已激活',
|
||
RENEWED: '已续期',
|
||
DEACTIVATED: '已停用',
|
||
NO_CHANGE: '无变化',
|
||
};
|
||
|
||
/**
|
||
* 将区域代码转换为名称
|
||
* @param code 区域代码,如 450000(省)或 451200(市)
|
||
* @returns 区域名称
|
||
*/
|
||
const getRegionName = (code: string | null | undefined): string => {
|
||
if (!code) return '-';
|
||
// 先尝试查找城市
|
||
if (CITY_CODE_NAMES[code]) {
|
||
return CITY_CODE_NAMES[code];
|
||
}
|
||
// 再尝试查找省份
|
||
if (PROVINCE_CODE_NAMES[code]) {
|
||
return PROVINCE_CODE_NAMES[code];
|
||
}
|
||
// 尝试去掉后4位的0(如 450000 -> 45)查找省份
|
||
if (code.length === 6 && code.endsWith('0000')) {
|
||
const shortCode = code.substring(0, 2);
|
||
if (PROVINCE_CODE_NAMES[shortCode]) {
|
||
return PROVINCE_CODE_NAMES[shortCode];
|
||
}
|
||
}
|
||
return code;
|
||
};
|
||
|
||
/**
|
||
* 用户详情页面
|
||
*/
|
||
export default function UserDetailPage() {
|
||
const params = useParams();
|
||
const router = useRouter();
|
||
const accountSequence = params.id as string;
|
||
|
||
const [activeTab, setActiveTab] = useState<TabType>('referral');
|
||
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
|
||
const [plantingPage, setPlantingPage] = useState(1);
|
||
const [walletPage, setWalletPage] = useState(1);
|
||
// 存储已展开节点的状态:key 是节点 accountSequence,value 是其子节点数组(null 表示未加载)
|
||
const [expandedNodes, setExpandedNodes] = useState<Record<string, ReferralNode[] | null>>({});
|
||
// [2026-02-05] 新增:合同信息状态
|
||
const [contractsPage, setContractsPage] = useState(1);
|
||
const [contractsData, setContractsData] = useState<ContractsListResponse | null>(null);
|
||
const [contractsLoading, setContractsLoading] = useState(false);
|
||
|
||
// 获取用户完整信息
|
||
const { data: userDetail, isLoading: detailLoading, error: detailError } = useUserFullDetail(accountSequence);
|
||
|
||
// 获取推荐关系树(以当前选中的用户为根)
|
||
const { data: referralTree, isLoading: treeLoading } = useReferralTree(treeRootUser, 'both', 1);
|
||
|
||
// 获取认种分类账
|
||
const { data: plantingData, isLoading: plantingLoading } = usePlantingLedger(accountSequence, {
|
||
page: plantingPage,
|
||
pageSize: 10,
|
||
});
|
||
|
||
// 获取钱包分类账
|
||
const { data: walletData, isLoading: walletLoading } = useWalletLedger(accountSequence, {
|
||
page: walletPage,
|
||
pageSize: 10,
|
||
});
|
||
|
||
// 获取授权信息
|
||
const { data: authData, isLoading: authLoading } = useAuthorizationDetail(accountSequence);
|
||
|
||
// 当 referralTree 数据加载完成后,自动展开当前用户的直推下级
|
||
useEffect(() => {
|
||
if (referralTree && referralTree.directReferrals.length > 0) {
|
||
setExpandedNodes((prev) => ({
|
||
...prev,
|
||
[referralTree.currentUser.accountSequence]: referralTree.directReferrals,
|
||
}));
|
||
}
|
||
}, [referralTree]);
|
||
|
||
// [2026-02-05] 新增:加载合同数据
|
||
useEffect(() => {
|
||
if (activeTab === 'contracts') {
|
||
setContractsLoading(true);
|
||
contractService.getUserContracts(accountSequence, {
|
||
page: contractsPage,
|
||
pageSize: 10,
|
||
})
|
||
.then((data) => {
|
||
setContractsData(data);
|
||
})
|
||
.catch((error) => {
|
||
console.error('获取合同列表失败:', error);
|
||
setContractsData(null);
|
||
})
|
||
.finally(() => {
|
||
setContractsLoading(false);
|
||
});
|
||
}
|
||
}, [activeTab, accountSequence, contractsPage]);
|
||
|
||
// 切换推荐关系树的根节点
|
||
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 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');
|
||
}, [router]);
|
||
|
||
// [2026-02-05] 新增:下载合同 PDF
|
||
const handleDownloadContract = useCallback((orderNo: string) => {
|
||
const downloadUrl = contractService.getDownloadUrl(orderNo);
|
||
window.open(downloadUrl, '_blank');
|
||
}, []);
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateStr: string | null) => {
|
||
if (!dateStr) return '-';
|
||
return new Date(dateStr).toLocaleString('zh-CN');
|
||
};
|
||
|
||
// 格式化金额
|
||
const formatAmount = (amount: string | null) => {
|
||
if (!amount) return '-';
|
||
const num = parseFloat(amount);
|
||
return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 8 });
|
||
};
|
||
|
||
if (detailLoading) {
|
||
return (
|
||
<PageContainer title="用户详情">
|
||
<div className={styles.loading}>加载中...</div>
|
||
</PageContainer>
|
||
);
|
||
}
|
||
|
||
if (detailError || !userDetail) {
|
||
return (
|
||
<PageContainer title="用户详情">
|
||
<div className={styles.error}>
|
||
<p>加载失败: {(detailError as Error)?.message || '用户不存在'}</p>
|
||
<Button onClick={handleBack}>返回列表</Button>
|
||
</div>
|
||
</PageContainer>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PageContainer title={`用户详情 - ${userDetail.accountSequence}`}>
|
||
<div className={styles.userDetail}>
|
||
{/* 返回按钮 */}
|
||
<div className={styles.userDetail__backBar}>
|
||
<button className={styles.userDetail__backBtn} onClick={handleBack}>
|
||
<span className={styles.userDetail__backIcon}>←</span>
|
||
返回用户列表
|
||
</button>
|
||
</div>
|
||
|
||
{/* 用户基本信息卡片 */}
|
||
<div className={styles.userDetail__basicCard}>
|
||
<div className={styles.userDetail__basicHeader}>
|
||
<div
|
||
className={styles.userDetail__avatar}
|
||
style={{ backgroundImage: `url(${userDetail.avatar || '/images/Data@2x.png'})` }}
|
||
>
|
||
<div
|
||
className={cn(
|
||
styles.userDetail__status,
|
||
userDetail.isOnline ? styles['userDetail__status--online'] : styles['userDetail__status--offline']
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className={styles.userDetail__basicInfo}>
|
||
<h1 className={styles.userDetail__nickname}>
|
||
{userDetail.nickname || '未设置昵称'}
|
||
<span className={cn(
|
||
styles.userDetail__statusBadge,
|
||
styles[`userDetail__statusBadge--${userDetail.status}`]
|
||
)}>
|
||
{userDetail.status === 'active' ? '正常' : userDetail.status === 'frozen' ? '冻结' : '停用'}
|
||
</span>
|
||
</h1>
|
||
<div className={styles.userDetail__basicMeta}>
|
||
<span>账户序号: <strong>{userDetail.accountSequence}</strong></span>
|
||
<span>手机号: {userDetail.phoneNumberMasked || '未绑定'}</span>
|
||
<span>KYC: {userDetail.kycStatus}</span>
|
||
</div>
|
||
<div className={styles.userDetail__basicMeta}>
|
||
<span>注册时间: {formatDate(userDetail.registeredAt)}</span>
|
||
<span>最后活跃: {formatDate(userDetail.lastActiveAt)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className={styles.userDetail__statsGrid}>
|
||
<div className={styles.userDetail__statCard}>
|
||
<span className={styles.userDetail__statLabel}>个人认种量</span>
|
||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.personalAdoptions)}</span>
|
||
</div>
|
||
<div className={styles.userDetail__statCard}>
|
||
<span className={styles.userDetail__statLabel}>团队认种量</span>
|
||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.teamAdoptions)}</span>
|
||
</div>
|
||
<div className={styles.userDetail__statCard}>
|
||
<span className={styles.userDetail__statLabel}>团队地址数</span>
|
||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.teamAddresses)}</span>
|
||
</div>
|
||
<div className={styles.userDetail__statCard}>
|
||
<span className={styles.userDetail__statLabel}>龙虎榜排名</span>
|
||
<span className={cn(
|
||
styles.userDetail__statValue,
|
||
userDetail.ranking && userDetail.ranking <= 10 && styles['userDetail__statValue--gold']
|
||
)}>
|
||
{userDetail.ranking ? formatRanking(userDetail.ranking) : '-'}
|
||
</span>
|
||
</div>
|
||
<div className={styles.userDetail__statCard}>
|
||
<span className={styles.userDetail__statLabel}>引荐人数</span>
|
||
<span className={styles.userDetail__statValue}>
|
||
{formatNumber(userDetail.referralInfo.directReferralCount)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 引荐人信息 */}
|
||
{userDetail.referralInfo.referrerSequence && (
|
||
<div className={styles.userDetail__referrerInfo}>
|
||
<span className={styles.userDetail__referrerLabel}>引荐人:</span>
|
||
<Link
|
||
href={`/users/${userDetail.referralInfo.referrerSequence}`}
|
||
className={styles.userDetail__referrerLink}
|
||
>
|
||
{userDetail.referralInfo.referrerSequence}
|
||
{userDetail.referralInfo.referrerNickname && ` (${userDetail.referralInfo.referrerNickname})`}
|
||
</Link>
|
||
<span className={styles.userDetail__referrerMeta}>
|
||
邀请码: {userDetail.referralInfo.usedReferralCode || '-'}
|
||
</span>
|
||
<span className={styles.userDetail__referrerMeta}>
|
||
层级深度: {userDetail.referralInfo.depth}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tab 切换 */}
|
||
<div className={styles.userDetail__tabs}>
|
||
{tabs.map((tab) => (
|
||
<button
|
||
key={tab.key}
|
||
className={cn(
|
||
styles.userDetail__tab,
|
||
activeTab === tab.key && styles['userDetail__tab--active']
|
||
)}
|
||
onClick={() => setActiveTab(tab.key)}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Tab 内容 */}
|
||
<div className={styles.userDetail__tabContent}>
|
||
{/* 引荐关系 Tab */}
|
||
{activeTab === 'referral' && (
|
||
<div className={styles.referralTab}>
|
||
<div className={styles.referralTab__header}>
|
||
<h3>引荐关系树</h3>
|
||
{treeRootUser !== accountSequence && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setTreeRootUser(accountSequence)}
|
||
>
|
||
返回当前用户
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{treeLoading ? (
|
||
<div className={styles.referralTab__loading}>加载中...</div>
|
||
) : referralTree ? (
|
||
<div className={styles.referralTree}>
|
||
{/* 向上的引荐人链 */}
|
||
<div className={styles.referralTree__ancestors}>
|
||
<div className={styles.referralTree__label}>引荐人链 (向上)</div>
|
||
{referralTree.ancestors.length > 0 ? (
|
||
<>
|
||
{/* 有上级时显示祖先节点列表 */}
|
||
<div className={styles.referralTree__nodeList}>
|
||
{referralTree.ancestors.map((ancestor, index) => (
|
||
<div key={ancestor.accountSequence} className={styles.referralTree__nodeWrapper}>
|
||
<button
|
||
className={styles.referralTree__node}
|
||
onClick={() => handleTreeNodeClick(ancestor)}
|
||
>
|
||
<span className={styles.referralTree__nodeSeq}>{ancestor.accountSequence}</span>
|
||
<span className={styles.referralTree__nodeNickname}>
|
||
{ancestor.nickname || '未设置'}
|
||
</span>
|
||
<span className={styles.referralTree__nodeAdoptions}>
|
||
本人认种: {formatNumber(ancestor.personalAdoptions)} / 团队认种: {formatNumber(ancestor.teamAdoptions)}
|
||
</span>
|
||
</button>
|
||
{index < referralTree.ancestors.length - 1 && (
|
||
<div className={styles.referralTree__connector}>↓</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className={styles.referralTree__connector}>↓</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
{/* 没有上级时显示总部节点 */}
|
||
<div className={styles.referralTree__headquarters}>
|
||
<span className={styles.referralTree__headquartersLabel}>总部</span>
|
||
</div>
|
||
<div className={styles.referralTree__connector}>↓</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* 当前用户及其递归下级 */}
|
||
<div className={styles.referralTree__currentWrapper}>
|
||
<ReferralNodeItem
|
||
node={referralTree.currentUser}
|
||
isCurrentUser={true}
|
||
isHighlight={referralTree.currentUser.accountSequence === accountSequence}
|
||
expandedNodes={expandedNodes}
|
||
onToggle={handleToggleNode}
|
||
onClick={handleTreeNodeClick}
|
||
/>
|
||
</div>
|
||
|
||
</div>
|
||
) : (
|
||
<div className={styles.referralTab__empty}>暂无引荐关系数据</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 认种信息 Tab */}
|
||
{activeTab === 'planting' && (
|
||
<div className={styles.plantingTab}>
|
||
{plantingLoading ? (
|
||
<div className={styles.plantingTab__loading}>加载中...</div>
|
||
) : plantingData ? (
|
||
<>
|
||
{/* 认种汇总 */}
|
||
<div className={styles.plantingTab__summary}>
|
||
<h3>认种汇总</h3>
|
||
<div className={styles.plantingTab__summaryGrid}>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>总订单数</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatNumber(plantingData.summary.totalOrders)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>总认种量</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatNumber(plantingData.summary.totalTreeCount)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>总金额 (绿积分)</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatAmount(plantingData.summary.totalAmount)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>有效认种量</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatNumber(plantingData.summary.effectiveTreeCount)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>首次认种</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatDate(plantingData.summary.firstPlantingAt)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>最近认种</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatDate(plantingData.summary.lastPlantingAt)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 认种分类账 */}
|
||
<div className={styles.plantingTab__ledger}>
|
||
<h3>认种分类账明细</h3>
|
||
<div className={styles.ledgerTable}>
|
||
<div className={styles.ledgerTable__header}>
|
||
<div className={styles.ledgerTable__cell}>订单号</div>
|
||
<div className={styles.ledgerTable__cell}>认种数量</div>
|
||
<div className={styles.ledgerTable__cell}>金额</div>
|
||
<div className={styles.ledgerTable__cell}>省市</div>
|
||
<div className={styles.ledgerTable__cell}>创建时间</div>
|
||
<div className={styles.ledgerTable__cell}>支付时间</div>
|
||
</div>
|
||
{plantingData.items.length === 0 ? (
|
||
<div className={styles.ledgerTable__empty}>暂无认种记录</div>
|
||
) : (
|
||
plantingData.items.map((item) => (
|
||
<div key={item.orderId} className={styles.ledgerTable__row}>
|
||
<div className={styles.ledgerTable__cell}>{item.orderNo}</div>
|
||
<div className={styles.ledgerTable__cell}>{formatNumber(item.treeCount)}</div>
|
||
<div className={styles.ledgerTable__cell}>{formatAmount(item.totalAmount)}</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{getRegionName(item.selectedProvince)} / {getRegionName(item.selectedCity)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>{formatDate(item.createdAt)}</div>
|
||
<div className={styles.ledgerTable__cell}>{formatDate(item.paidAt)}</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* 分页 */}
|
||
{plantingData.totalPages > 1 && (
|
||
<div className={styles.pagination}>
|
||
<button
|
||
disabled={plantingPage === 1}
|
||
onClick={() => setPlantingPage((p) => p - 1)}
|
||
>
|
||
上一页
|
||
</button>
|
||
<span>第 {plantingPage} / {plantingData.totalPages} 页</span>
|
||
<button
|
||
disabled={plantingPage === plantingData.totalPages}
|
||
onClick={() => setPlantingPage((p) => p + 1)}
|
||
>
|
||
下一页
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className={styles.plantingTab__empty}>暂无认种数据</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 钱包信息 Tab */}
|
||
{activeTab === 'wallet' && (
|
||
<div className={styles.walletTab}>
|
||
{walletLoading ? (
|
||
<div className={styles.walletTab__loading}>加载中...</div>
|
||
) : walletData ? (
|
||
<>
|
||
{/* 钱包汇总 */}
|
||
<div className={styles.walletTab__summary}>
|
||
<h3>钱包汇总</h3>
|
||
<div className={styles.walletTab__summaryGrid}>
|
||
<div className={styles.walletTab__summaryItem}>
|
||
<span className={styles.walletTab__summaryLabel}>绿积分 可用</span>
|
||
<span className={styles.walletTab__summaryValue}>
|
||
{formatAmount(walletData.summary.usdtAvailable)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.walletTab__summaryItem}>
|
||
<span className={styles.walletTab__summaryLabel}>绿积分 冻结</span>
|
||
<span className={styles.walletTab__summaryValue}>
|
||
{formatAmount(walletData.summary.usdtFrozen)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.walletTab__summaryItem}>
|
||
<span className={styles.walletTab__summaryLabel}>待领取收益</span>
|
||
<span className={styles.walletTab__summaryValue}>
|
||
{formatAmount(walletData.summary.pendingUsdt)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.walletTab__summaryItem}>
|
||
<span className={styles.walletTab__summaryLabel}>可结算收益</span>
|
||
<span className={styles.walletTab__summaryValue}>
|
||
{formatAmount(walletData.summary.settleableUsdt)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.walletTab__summaryItem}>
|
||
<span className={styles.walletTab__summaryLabel}>已结算收益</span>
|
||
<span className={styles.walletTab__summaryValue}>
|
||
{formatAmount(walletData.summary.settledTotalUsdt)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.walletTab__summaryItem}>
|
||
<span className={styles.walletTab__summaryLabel}>过期收益</span>
|
||
<span className={styles.walletTab__summaryValue}>
|
||
{formatAmount(walletData.summary.expiredTotalUsdt)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 钱包分类账 */}
|
||
<div className={styles.walletTab__ledger}>
|
||
<h3>钱包分类账明细</h3>
|
||
<div className={styles.ledgerTable}>
|
||
<div className={styles.ledgerTable__header}>
|
||
<div className={styles.ledgerTable__cell}>流水ID</div>
|
||
<div className={styles.ledgerTable__cell}>类型</div>
|
||
<div className={styles.ledgerTable__cell}>资产</div>
|
||
<div className={styles.ledgerTable__cell}>金额</div>
|
||
<div className={styles.ledgerTable__cell}>余额快照</div>
|
||
<div className={styles.ledgerTable__cell}>关联订单</div>
|
||
<div className={styles.ledgerTable__cell}>备注</div>
|
||
<div className={styles.ledgerTable__cell}>时间</div>
|
||
</div>
|
||
{walletData.items.length === 0 ? (
|
||
<div className={styles.ledgerTable__empty}>暂无钱包流水</div>
|
||
) : (
|
||
walletData.items.map((item) => (
|
||
<div key={item.entryId} className={styles.ledgerTable__row}>
|
||
<div className={styles.ledgerTable__cell}>{item.entryId}</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{entryTypeLabels[item.entryType] || item.entryType}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{assetTypeLabels[item.assetType] || item.assetType}
|
||
</div>
|
||
<div className={cn(
|
||
styles.ledgerTable__cell,
|
||
parseFloat(item.amount) >= 0
|
||
? styles['ledgerTable__cell--positive']
|
||
: styles['ledgerTable__cell--negative']
|
||
)}>
|
||
{parseFloat(item.amount) >= 0 ? '+' : ''}{formatAmount(item.amount)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{formatAmount(item.balanceAfter)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{item.refOrderId || item.refTxHash || '-'}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{item.memo || '-'}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>{formatDate(item.createdAt)}</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* 分页 */}
|
||
{walletData.totalPages > 1 && (
|
||
<div className={styles.pagination}>
|
||
<button
|
||
disabled={walletPage === 1}
|
||
onClick={() => setWalletPage((p) => p - 1)}
|
||
>
|
||
上一页
|
||
</button>
|
||
<span>第 {walletPage} / {walletData.totalPages} 页</span>
|
||
<button
|
||
disabled={walletPage === walletData.totalPages}
|
||
onClick={() => setWalletPage((p) => p + 1)}
|
||
>
|
||
下一页
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className={styles.walletTab__empty}>暂无钱包数据</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 授权信息 Tab */}
|
||
{activeTab === 'authorization' && (
|
||
<div className={styles.authTab}>
|
||
{authLoading ? (
|
||
<div className={styles.authTab__loading}>加载中...</div>
|
||
) : authData ? (
|
||
<>
|
||
{/* 授权角色列表 */}
|
||
<div className={styles.authTab__roles}>
|
||
<h3>授权角色</h3>
|
||
{authData.roles.length === 0 ? (
|
||
<div className={styles.authTab__empty}>暂无授权角色</div>
|
||
) : (
|
||
<div className={styles.authTab__roleGrid}>
|
||
{authData.roles.map((role) => (
|
||
<div key={role.id} className={styles.authTab__roleCard}>
|
||
<div className={styles.authTab__roleHeader}>
|
||
<span className={styles.authTab__roleType}>
|
||
{roleTypeLabels[role.roleType] || role.roleType}
|
||
</span>
|
||
<span className={cn(
|
||
styles.authTab__roleStatus,
|
||
styles[`authTab__roleStatus--${role.status.toLowerCase()}`]
|
||
)}>
|
||
{authStatusLabels[role.status] || role.status}
|
||
</span>
|
||
</div>
|
||
<div className={styles.authTab__roleInfo}>
|
||
<p><strong>{getRegionLabel(role.roleType)}</strong> {role.regionName} ({role.regionCode})</p>
|
||
<p><strong>显示头衔:</strong> {role.displayTitle}</p>
|
||
<p>
|
||
<strong>权益状态:</strong>
|
||
{role.benefitActive ? (
|
||
<span className={styles.authTab__benefitActive}>已激活</span>
|
||
) : (
|
||
<span className={styles.authTab__benefitInactive}>未激活</span>
|
||
)}
|
||
</p>
|
||
<p><strong>初始目标:</strong> {formatNumber(role.initialTargetTreeCount)} 棵</p>
|
||
<p><strong>月度目标类型:</strong> {role.monthlyTargetType}</p>
|
||
<p><strong>授权时间:</strong> {formatDate(role.authorizedAt)}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 月度考核记录 */}
|
||
<div className={styles.authTab__assessments}>
|
||
<h3>月度考核记录</h3>
|
||
{(() => {
|
||
// 只显示用户实际拥有且未撤销角色的考核记录(按 authorization_id 匹配)
|
||
const activeRoleIds = new Set(
|
||
authData.roles
|
||
.filter(r => r.status !== 'REVOKED')
|
||
.map(r => r.id)
|
||
);
|
||
const filteredAssessments = authData.assessments.filter(
|
||
a => activeRoleIds.has(a.authorizationId)
|
||
);
|
||
|
||
// 创建角色ID到区域名称的映射,用于显示角色的区域信息
|
||
const roleIdToRegion = new Map(
|
||
authData.roles.map(r => [r.id, r.regionName])
|
||
);
|
||
|
||
return filteredAssessments.length === 0 ? (
|
||
<div className={styles.authTab__empty}>暂无考核记录</div>
|
||
) : (
|
||
<div className={styles.ledgerTable}>
|
||
<div className={styles.ledgerTable__header}>
|
||
<div className={styles.ledgerTable__cell}>考核月份</div>
|
||
<div className={styles.ledgerTable__cell}>角色</div>
|
||
<div className={styles.ledgerTable__cell}>区域</div>
|
||
<div className={styles.ledgerTable__cell}>月度目标/完成</div>
|
||
<div className={styles.ledgerTable__cell}>累计目标/完成</div>
|
||
<div className={styles.ledgerTable__cell}>结果</div>
|
||
<div className={styles.ledgerTable__cell}>区域排名</div>
|
||
</div>
|
||
{filteredAssessments.map((assessment) => (
|
||
<div key={assessment.id} className={styles.ledgerTable__row}>
|
||
<div className={styles.ledgerTable__cell}>{assessment.assessmentMonth}</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{roleTypeLabels[assessment.roleType] || assessment.roleType}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{roleIdToRegion.get(assessment.authorizationId) || getRegionName(assessment.regionCode)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{formatNumber(assessment.monthlyCompleted)} / {formatNumber(assessment.monthlyTarget)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{formatNumber(assessment.cumulativeCompleted)} / {formatNumber(assessment.cumulativeTarget)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
<span className={cn(
|
||
styles.ledgerTable__result,
|
||
styles[`ledgerTable__result--${assessment.result.toLowerCase()}`]
|
||
)}>
|
||
{assessmentResultLabels[assessment.result] || assessment.result}
|
||
</span>
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{assessment.rankingInRegion || '-'}
|
||
{assessment.isFirstPlace && ' 🥇'}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
})()}
|
||
</div>
|
||
|
||
{/* 权益考核记录 */}
|
||
<div className={styles.authTab__assessments}>
|
||
<h3>权益考核记录</h3>
|
||
{(() => {
|
||
// 只显示用户实际拥有且未撤销角色的权益考核记录
|
||
const activeRoleIds = new Set(
|
||
authData.roles
|
||
.filter(r => r.status !== 'REVOKED')
|
||
.map(r => r.id)
|
||
);
|
||
const filteredBenefitAssessments = (authData.benefitAssessments || []).filter(
|
||
a => activeRoleIds.has(a.authorizationId)
|
||
);
|
||
|
||
// 创建角色ID到区域名称的映射
|
||
const roleIdToRegion = new Map(
|
||
authData.roles.map(r => [r.id, r.regionName])
|
||
);
|
||
|
||
return filteredBenefitAssessments.length === 0 ? (
|
||
<div className={styles.authTab__empty}>暂无权益考核记录</div>
|
||
) : (
|
||
<div className={styles.ledgerTable}>
|
||
<div className={styles.ledgerTable__header}>
|
||
<div className={styles.ledgerTable__cell}>考核月份</div>
|
||
<div className={styles.ledgerTable__cell}>角色</div>
|
||
<div className={styles.ledgerTable__cell}>区域</div>
|
||
<div className={styles.ledgerTable__cell}>完成/需求</div>
|
||
<div className={styles.ledgerTable__cell}>权益操作</div>
|
||
<div className={styles.ledgerTable__cell}>权益状态</div>
|
||
<div className={styles.ledgerTable__cell}>有效期至</div>
|
||
<div className={styles.ledgerTable__cell}>结果</div>
|
||
</div>
|
||
{filteredBenefitAssessments.map((assessment) => (
|
||
<div key={assessment.id} className={styles.ledgerTable__row}>
|
||
<div className={styles.ledgerTable__cell}>{assessment.assessmentMonth}</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{roleTypeLabels[assessment.roleType] || assessment.roleType}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{roleIdToRegion.get(assessment.authorizationId) || assessment.regionName || getRegionName(assessment.regionCode)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{formatNumber(assessment.treesCompleted)} / {formatNumber(assessment.treesRequired)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
<span className={cn(
|
||
styles.ledgerTable__benefitAction,
|
||
styles[`ledgerTable__benefitAction--${assessment.benefitActionTaken.toLowerCase()}`]
|
||
)}>
|
||
{benefitActionLabels[assessment.benefitActionTaken] || assessment.benefitActionTaken}
|
||
</span>
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{assessment.previousBenefitStatus ? '有效' : '无效'} → {assessment.newBenefitStatus ? '有效' : '无效'}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{assessment.newValidUntil ? formatDate(assessment.newValidUntil) : '-'}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
<span className={cn(
|
||
styles.ledgerTable__result,
|
||
styles[`ledgerTable__result--${assessment.result.toLowerCase()}`]
|
||
)}>
|
||
{assessmentResultLabels[assessment.result] || assessment.result}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
})()}
|
||
</div>
|
||
|
||
{/* 系统账户流水(如果有) */}
|
||
{authData.systemAccountLedger.length > 0 && (
|
||
<div className={styles.authTab__systemLedger}>
|
||
<h3>系统账户流水</h3>
|
||
<div className={styles.ledgerTable}>
|
||
<div className={styles.ledgerTable__header}>
|
||
<div className={styles.ledgerTable__cell}>流水ID</div>
|
||
<div className={styles.ledgerTable__cell}>账户类型</div>
|
||
<div className={styles.ledgerTable__cell}>流水类型</div>
|
||
<div className={styles.ledgerTable__cell}>金额</div>
|
||
<div className={styles.ledgerTable__cell}>余额</div>
|
||
<div className={styles.ledgerTable__cell}>时间</div>
|
||
</div>
|
||
{authData.systemAccountLedger.map((ledger) => (
|
||
<div key={ledger.ledgerId} className={styles.ledgerTable__row}>
|
||
<div className={styles.ledgerTable__cell}>{ledger.ledgerId}</div>
|
||
<div className={styles.ledgerTable__cell}>{ledger.accountType}</div>
|
||
<div className={styles.ledgerTable__cell}>{ledger.entryType}</div>
|
||
<div className={cn(
|
||
styles.ledgerTable__cell,
|
||
parseFloat(ledger.amount) >= 0
|
||
? styles['ledgerTable__cell--positive']
|
||
: styles['ledgerTable__cell--negative']
|
||
)}>
|
||
{parseFloat(ledger.amount) >= 0 ? '+' : ''}{formatAmount(ledger.amount)}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>{formatAmount(ledger.balanceAfter)}</div>
|
||
<div className={styles.ledgerTable__cell}>{formatDate(ledger.createdAt)}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className={styles.authTab__empty}>暂无授权数据</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* [2026-02-05] 新增:合同信息 Tab */}
|
||
{activeTab === 'contracts' && (
|
||
<div className={styles.plantingTab}>
|
||
{contractsLoading ? (
|
||
<div className={styles.plantingTab__loading}>加载中...</div>
|
||
) : contractsData ? (
|
||
<>
|
||
{/* 合同汇总 */}
|
||
<div className={styles.plantingTab__summary}>
|
||
<h3>合同汇总</h3>
|
||
<div className={styles.plantingTab__summaryGrid}>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>合同总数</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatNumber(contractsData.total)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>已签署</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatNumber(contractsData.items.filter(c => c.status === 'SIGNED').length)}
|
||
</span>
|
||
</div>
|
||
<div className={styles.plantingTab__summaryItem}>
|
||
<span className={styles.plantingTab__summaryLabel}>待签署</span>
|
||
<span className={styles.plantingTab__summaryValue}>
|
||
{formatNumber(contractsData.items.filter(c => ['PENDING', 'SCROLLED', 'ACKNOWLEDGED'].includes(c.status)).length)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 合同列表 */}
|
||
<div className={styles.plantingTab__ledger}>
|
||
<h3>合同列表</h3>
|
||
<div className={styles.ledgerTable}>
|
||
<div className={styles.ledgerTable__header}>
|
||
<div className={styles.ledgerTable__cell}>合同编号</div>
|
||
<div className={styles.ledgerTable__cell}>订单号</div>
|
||
<div className={styles.ledgerTable__cell}>认种数量</div>
|
||
<div className={styles.ledgerTable__cell}>金额</div>
|
||
<div className={styles.ledgerTable__cell}>省市</div>
|
||
<div className={styles.ledgerTable__cell}>状态</div>
|
||
<div className={styles.ledgerTable__cell}>签署时间</div>
|
||
<div className={styles.ledgerTable__cell}>操作</div>
|
||
</div>
|
||
{contractsData.items.length === 0 ? (
|
||
<div className={styles.ledgerTable__empty}>暂无合同记录</div>
|
||
) : (
|
||
contractsData.items.map((contract) => (
|
||
<div key={contract.orderNo} className={styles.ledgerTable__row}>
|
||
<div className={styles.ledgerTable__cell}>{contract.contractNo}</div>
|
||
<div className={styles.ledgerTable__cell}>{contract.orderNo}</div>
|
||
<div className={styles.ledgerTable__cell}>{formatNumber(contract.treeCount)}</div>
|
||
<div className={styles.ledgerTable__cell}>{formatAmount(contract.totalAmount.toString())}</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{contract.provinceName} / {contract.cityName}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell} style={getContractStatusStyle(contract.status)}>
|
||
{CONTRACT_STATUS_LABELS[contract.status] || contract.status}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{contract.signedAt ? formatDate(contract.signedAt) : '-'}
|
||
</div>
|
||
<div className={styles.ledgerTable__cell}>
|
||
{contract.status === 'SIGNED' && contract.signedPdfUrl && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleDownloadContract(contract.orderNo)}
|
||
>
|
||
下载PDF
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* 分页 */}
|
||
{contractsData.totalPages > 1 && (
|
||
<div className={styles.pagination}>
|
||
<button
|
||
disabled={contractsPage === 1}
|
||
onClick={() => setContractsPage((p) => p - 1)}
|
||
>
|
||
上一页
|
||
</button>
|
||
<span>第 {contractsPage} / {contractsData.totalPages} 页</span>
|
||
<button
|
||
disabled={contractsPage === contractsData.totalPages}
|
||
onClick={() => setContractsPage((p) => p + 1)}
|
||
>
|
||
下一页
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className={styles.plantingTab__empty}>暂无合同数据</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</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)} / 团队认种: {formatNumber(node.teamAdoptions)}
|
||
</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>
|
||
);
|
||
}
|