rwadurian/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx

1167 lines
53 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 是节点 accountSequencevalue 是其子节点数组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>
);
}