feat(mining-admin-web): 复用admin-web用户管理功能
- 更新用户列表:添加头像、个人/团队认种、推荐人、状态徽章 - 更新用户详情:添加头像、KYC状态、认种统计卡片 - 新增引荐关系Tab:展示引荐人链和直推下级树 - 新增认种信息Tab:认种汇总和认种分类账明细 - 新增钱包信息Tab:钱包汇总和钱包分类账明细 - 更新类型定义和API hooks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5dab829995
commit
8fc527b918
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { useUserDetail } from '@/features/users/hooks/use-users';
|
import { useUserDetail } from '@/features/users/hooks/use-users';
|
||||||
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
||||||
|
|
@ -8,9 +9,15 @@ import { formatDateTime } from '@/lib/utils/date';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ContributionRecordsList } from '@/features/users/components/contribution-records-list';
|
import { ContributionRecordsList } from '@/features/users/components/contribution-records-list';
|
||||||
import { MiningRecordsList } from '@/features/users/components/mining-records-list';
|
import { MiningRecordsList } from '@/features/users/components/mining-records-list';
|
||||||
import { TradeOrdersList } from '@/features/users/components/trade-orders-list';
|
import { TradeOrdersList } from '@/features/users/components/trade-orders-list';
|
||||||
|
import { ReferralTree } from '@/features/users/components/referral-tree';
|
||||||
|
import { PlantingLedger } from '@/features/users/components/planting-ledger';
|
||||||
|
import { WalletLedger } from '@/features/users/components/wallet-ledger';
|
||||||
|
import { Users, TreePine, Wallet, Zap, ShoppingCart, Network, Coins } from 'lucide-react';
|
||||||
|
|
||||||
function UserDetailSkeleton() {
|
function UserDetailSkeleton() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -45,62 +52,128 @@ export default function UserDetailPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取状态徽章
|
||||||
|
const getStatusBadge = () => {
|
||||||
|
if (user?.status === 'frozen') {
|
||||||
|
return <Badge variant="destructive">冻结</Badge>;
|
||||||
|
}
|
||||||
|
if (user?.status === 'deactivated') {
|
||||||
|
return <Badge variant="secondary">停用</Badge>;
|
||||||
|
}
|
||||||
|
if (user?.isOnline) {
|
||||||
|
return <Badge className="bg-green-500">在线</Badge>;
|
||||||
|
}
|
||||||
|
return <Badge variant="outline">正常</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader title="用户详情" description={`账户序列: ${accountSequence}`} backLink="/users" />
|
<PageHeader title="用户详情" description={`账户序列: ${accountSequence}`} backLink="/users" />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* 基本信息卡片 */}
|
||||||
<Card className="lg:col-span-2">
|
<Card className="lg:col-span-2">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">基本信息</CardTitle>
|
<CardTitle className="text-lg">基本信息</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
<div className="flex items-start gap-6 mb-6">
|
||||||
<div>
|
<div className="relative">
|
||||||
<p className="text-sm text-muted-foreground">账户序列</p>
|
<Avatar className="h-20 w-20">
|
||||||
<p className="font-mono font-medium">{user?.accountSequence}</p>
|
<AvatarImage src={user?.avatar || undefined} alt={user?.nickname || ''} />
|
||||||
|
<AvatarFallback className="text-2xl">{user?.nickname?.charAt(0) || 'U'}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{user?.isOnline && (
|
||||||
|
<span className="absolute bottom-1 right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<p className="text-sm text-muted-foreground">昵称</p>
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<p className="font-medium">{user?.nickname || '-'}</p>
|
<h2 className="text-xl font-semibold">{user?.nickname || '未设置昵称'}</h2>
|
||||||
|
{getStatusBadge()}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">账户序列:</span>
|
||||||
|
<span className="ml-2 font-mono font-medium">{user?.accountSequence}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">手机号:</span>
|
||||||
|
<span className="ml-2">{user?.phoneNumberMasked || user?.phone}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">KYC状态:</span>
|
||||||
|
<span className="ml-2">{user?.kycStatus || '未认证'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">手机号</p>
|
|
||||||
<p className="font-medium">{user?.phone}</p>
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<Users className="h-3 w-3" /> 推荐人
|
||||||
|
</p>
|
||||||
|
{user?.referrerAccountSequence ? (
|
||||||
|
<Link
|
||||||
|
href={`/users/${user.referrerAccountSequence}`}
|
||||||
|
className="font-mono font-medium text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{user.referrerAccountSequence}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<p className="font-medium">-</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
<p className="text-sm text-muted-foreground">认种状态</p>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
|
||||||
user?.hasAdopted ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{user?.hasAdopted ? '已认种' : '未认种'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">推荐人</p>
|
|
||||||
<p className="font-mono font-medium">{user?.referrerAccountSequence || '-'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">直推人数</p>
|
<p className="text-sm text-muted-foreground">直推人数</p>
|
||||||
<p className="font-medium">{formatNumber(user?.directReferralCount)}</p>
|
<p className="font-medium">{formatNumber(user?.directReferralCount)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
<p className="text-sm text-muted-foreground">直推认种</p>
|
<p className="text-sm text-muted-foreground">直推认种</p>
|
||||||
<p className="font-medium">{formatNumber(user?.directReferralAdoptedCount)}</p>
|
<p className="font-medium">{formatNumber(user?.directReferralAdoptedCount)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
<p className="text-sm text-muted-foreground">注册时间</p>
|
<p className="text-sm text-muted-foreground">注册时间</p>
|
||||||
<p className="text-sm">{formatDateTime(user?.createdAt)}</p>
|
<p className="text-sm">{formatDateTime(user?.registeredAt || user?.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 认种统计 */}
|
||||||
|
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<TreePine className="h-3 w-3 text-green-600" /> 个人认种
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold text-green-600">{formatNumber(user?.personalAdoptions ?? 0)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<Users className="h-3 w-3 text-blue-600" /> 团队认种
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold text-blue-600">{formatNumber(user?.teamAdoptions ?? 0)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">团队地址</p>
|
||||||
|
<p className="text-lg font-bold text-purple-600">{formatNumber(user?.teamAddresses ?? 0)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-amber-50 dark:bg-amber-950 rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">龙虎榜排名</p>
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{user?.ranking ? `#${user.ranking}` : '-'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 算力构成卡片 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">算力构成</CardTitle>
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5 text-primary" />
|
||||||
|
算力构成
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
@ -128,45 +201,102 @@ export default function UserDetailPage() {
|
||||||
<span className="font-mono">{formatDecimal(user?.contributions?.teamBonus, 4)}</span>
|
<span className="font-mono">{formatDecimal(user?.contributions?.teamBonus, 4)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between pt-3 border-t font-medium">
|
<div className="flex justify-between pt-3 border-t font-medium">
|
||||||
<span>有效算力</span>
|
<span>有效算力 (贡献值)</span>
|
||||||
<span className="font-mono text-primary">{formatDecimal(user?.effectiveContribution, 4)}</span>
|
<span className="font-mono text-primary text-lg">{formatDecimal(user?.effectiveContribution, 4)}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 余额卡片 */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6 text-center">
|
<CardContent className="p-6">
|
||||||
<p className="text-sm text-muted-foreground">挖矿余额</p>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-2xl font-bold font-mono text-primary">{formatDecimal(user?.miningBalance, 4)}</p>
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<Coins className="h-4 w-4" /> 挖矿余额
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold font-mono text-primary mt-1">
|
||||||
|
{formatDecimal(user?.miningBalance, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6 text-center">
|
<CardContent className="p-6">
|
||||||
<p className="text-sm text-muted-foreground">交易余额</p>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-2xl font-bold font-mono text-blue-600">{formatDecimal(user?.tradingBalance, 4)}</p>
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<ShoppingCart className="h-4 w-4" /> 交易余额
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold font-mono text-blue-600 mt-1">
|
||||||
|
{formatDecimal(user?.tradingBalance, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6 text-center">
|
<CardContent className="p-6">
|
||||||
<p className="text-sm text-muted-foreground">冻结余额</p>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-2xl font-bold font-mono text-orange-600">{formatDecimal(user?.frozenBalance, 4)}</p>
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">冻结余额</p>
|
||||||
|
<p className="text-3xl font-bold font-mono text-orange-600 mt-1">
|
||||||
|
{formatDecimal(user?.frozenBalance, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tab 区域 */}
|
||||||
<Tabs defaultValue="contributions">
|
<Tabs defaultValue="contributions">
|
||||||
<TabsList>
|
<TabsList className="grid w-full grid-cols-6">
|
||||||
<TabsTrigger value="contributions">算力记录</TabsTrigger>
|
<TabsTrigger value="contributions" className="flex items-center gap-1">
|
||||||
<TabsTrigger value="mining">挖矿记录</TabsTrigger>
|
<Zap className="h-4 w-4" />
|
||||||
<TabsTrigger value="trading">交易订单</TabsTrigger>
|
<span className="hidden sm:inline">算力记录</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="referral" className="flex items-center gap-1">
|
||||||
|
<Network className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">引荐关系</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="planting" className="flex items-center gap-1">
|
||||||
|
<TreePine className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">认种信息</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="wallet" className="flex items-center gap-1">
|
||||||
|
<Wallet className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">钱包信息</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="mining" className="flex items-center gap-1">
|
||||||
|
<Coins className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">挖矿记录</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="trading" className="flex items-center gap-1">
|
||||||
|
<ShoppingCart className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">交易订单</span>
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="contributions" className="mt-4">
|
<TabsContent value="contributions" className="mt-4">
|
||||||
<ContributionRecordsList accountSequence={accountSequence} />
|
<ContributionRecordsList accountSequence={accountSequence} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="referral" className="mt-4">
|
||||||
|
<ReferralTree accountSequence={accountSequence} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="planting" className="mt-4">
|
||||||
|
<PlantingLedger accountSequence={accountSequence} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="wallet" className="mt-4">
|
||||||
|
<WalletLedger accountSequence={accountSequence} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="mining" className="mt-4">
|
<TabsContent value="mining" className="mt-4">
|
||||||
<MiningRecordsList accountSequence={accountSequence} />
|
<MiningRecordsList accountSequence={accountSequence} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { useUsers } from '@/features/users/hooks/use-users';
|
import { useUsers } from '@/features/users/hooks/use-users';
|
||||||
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
||||||
|
|
@ -10,8 +11,10 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Search, ChevronLeft, ChevronRight, Eye } from 'lucide-react';
|
import { Search, ChevronLeft, ChevronRight, Eye, Users, TreePine } from 'lucide-react';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
|
|
@ -32,9 +35,23 @@ export default function UsersPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取状态徽章样式
|
||||||
|
const getStatusBadge = (status?: string, isOnline?: boolean) => {
|
||||||
|
if (status === 'frozen') {
|
||||||
|
return <Badge variant="destructive" className="text-xs">冻结</Badge>;
|
||||||
|
}
|
||||||
|
if (status === 'deactivated') {
|
||||||
|
return <Badge variant="secondary" className="text-xs">停用</Badge>;
|
||||||
|
}
|
||||||
|
if (isOnline) {
|
||||||
|
return <Badge className="bg-green-500 text-xs">在线</Badge>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader title="用户管理" description="查看和管理用户的算力、积分股等信息" />
|
<PageHeader title="用户管理" description="查看和管理用户的算力、贡献值、认种等信息" />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
|
|
@ -56,70 +73,127 @@ export default function UsersPage() {
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Table>
|
<div className="overflow-x-auto">
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>账户序列</TableHead>
|
|
||||||
<TableHead>昵称</TableHead>
|
|
||||||
<TableHead>手机号</TableHead>
|
|
||||||
<TableHead>认种状态</TableHead>
|
|
||||||
<TableHead className="text-right">有效算力</TableHead>
|
|
||||||
<TableHead className="text-right">挖矿余额</TableHead>
|
|
||||||
<TableHead className="text-right">交易余额</TableHead>
|
|
||||||
<TableHead>注册时间</TableHead>
|
|
||||||
<TableHead className="w-[80px]">操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{isLoading ? (
|
|
||||||
[...Array(10)].map((_, i) => (
|
|
||||||
<TableRow key={i}>
|
|
||||||
{[...Array(9)].map((_, j) => (
|
|
||||||
<TableCell key={j}>
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : data?.items.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
|
<TableHead className="w-[60px]">头像</TableHead>
|
||||||
暂无数据
|
<TableHead>账户序列</TableHead>
|
||||||
</TableCell>
|
<TableHead>昵称</TableHead>
|
||||||
|
<TableHead>手机号</TableHead>
|
||||||
|
<TableHead className="text-center">认种状态</TableHead>
|
||||||
|
<TableHead className="text-right">个人认种</TableHead>
|
||||||
|
<TableHead className="text-right">团队认种</TableHead>
|
||||||
|
<TableHead className="text-right">有效算力</TableHead>
|
||||||
|
<TableHead className="text-right">挖矿余额</TableHead>
|
||||||
|
<TableHead>推荐人</TableHead>
|
||||||
|
<TableHead>注册时间</TableHead>
|
||||||
|
<TableHead className="w-[80px]">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
data?.items.map((user) => (
|
<TableBody>
|
||||||
<TableRow key={user.accountSequence}>
|
{isLoading ? (
|
||||||
<TableCell className="font-mono">{user.accountSequence}</TableCell>
|
[...Array(10)].map((_, i) => (
|
||||||
<TableCell>{user.nickname || '-'}</TableCell>
|
<TableRow key={i}>
|
||||||
<TableCell>{user.phone}</TableCell>
|
{[...Array(12)].map((_, j) => (
|
||||||
<TableCell>
|
<TableCell key={j}>
|
||||||
<span
|
<Skeleton className="h-4 w-full" />
|
||||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
</TableCell>
|
||||||
user.hasAdopted ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
|
))}
|
||||||
}`}
|
</TableRow>
|
||||||
>
|
))
|
||||||
{user.hasAdopted ? '已认种' : '未认种'}
|
) : data?.items.length === 0 ? (
|
||||||
</span>
|
<TableRow>
|
||||||
</TableCell>
|
<TableCell colSpan={12} className="text-center py-8 text-muted-foreground">
|
||||||
<TableCell className="text-right font-mono">
|
暂无数据
|
||||||
{formatDecimal(user.effectiveContribution, 4)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right font-mono">{formatDecimal(user.miningBalance, 4)}</TableCell>
|
|
||||||
<TableCell className="text-right font-mono">{formatDecimal(user.tradingBalance, 4)}</TableCell>
|
|
||||||
<TableCell className="text-sm">{formatDateTime(user.createdAt)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Link href={`/users/${user.accountSequence}`}>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
data?.items.map((user) => (
|
||||||
</TableBody>
|
<TableRow key={user.accountSequence}>
|
||||||
</Table>
|
{/* 头像 */}
|
||||||
|
<TableCell>
|
||||||
|
<div className="relative">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src={user.avatar || undefined} alt={user.nickname || ''} />
|
||||||
|
<AvatarFallback className="text-xs">
|
||||||
|
{user.nickname?.charAt(0) || 'U'}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{user.isOnline && (
|
||||||
|
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 rounded-full border-2 border-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{/* 账户序列 */}
|
||||||
|
<TableCell className="font-mono text-sm">{user.accountSequence}</TableCell>
|
||||||
|
{/* 昵称 */}
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="max-w-[120px] truncate">{user.nickname || '-'}</span>
|
||||||
|
{getStatusBadge(user.status, user.isOnline)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{/* 手机号 */}
|
||||||
|
<TableCell className="text-sm">{user.phoneNumberMasked || user.phone}</TableCell>
|
||||||
|
{/* 认种状态 */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
user.hasAdopted ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{user.hasAdopted ? '已认种' : '未认种'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 个人认种 */}
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<TreePine className="h-3 w-3 text-green-600" />
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
{formatNumber(user.personalAdoptions ?? 0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{/* 团队认种 */}
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<Users className="h-3 w-3 text-blue-600" />
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
{formatNumber(user.teamAdoptions ?? 0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{/* 有效算力 */}
|
||||||
|
<TableCell className="text-right font-mono text-sm text-primary font-medium">
|
||||||
|
{formatDecimal(user.effectiveContribution, 4)}
|
||||||
|
</TableCell>
|
||||||
|
{/* 挖矿余额 */}
|
||||||
|
<TableCell className="text-right font-mono text-sm">
|
||||||
|
{formatDecimal(user.miningBalance, 4)}
|
||||||
|
</TableCell>
|
||||||
|
{/* 推荐人 */}
|
||||||
|
<TableCell className="font-mono text-sm text-muted-foreground">
|
||||||
|
{user.referrerId || user.referrerAccountSequence || '-'}
|
||||||
|
</TableCell>
|
||||||
|
{/* 注册时间 */}
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{formatDateTime(user.createdAt)}
|
||||||
|
</TableCell>
|
||||||
|
{/* 操作 */}
|
||||||
|
<TableCell>
|
||||||
|
<Link href={`/users/${user.accountSequence}`}>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{data && data.totalPages > 1 && (
|
{data && data.totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between p-4 border-t">
|
<div className="flex items-center justify-between p-4 border-t">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
import type { UserOverview, UserDetail, ContributionRecord, MiningRecord, TradeOrder } from '@/types/user';
|
import type {
|
||||||
|
UserOverview,
|
||||||
|
UserDetail,
|
||||||
|
ContributionRecord,
|
||||||
|
MiningRecord,
|
||||||
|
TradeOrder,
|
||||||
|
ReferralTreeData,
|
||||||
|
PlantingLedgerResponse,
|
||||||
|
WalletLedgerResponse,
|
||||||
|
} from '@/types/user';
|
||||||
import type { PaginatedResponse, PaginationParams } from '@/types/api';
|
import type { PaginatedResponse, PaginationParams } from '@/types/api';
|
||||||
|
|
||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
|
|
@ -36,4 +45,32 @@ export const usersApi = {
|
||||||
const response = await apiClient.get(`/users/${accountSequence}/orders`, { params });
|
const response = await apiClient.get(`/users/${accountSequence}/orders`, { params });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 从 admin-web 复用的 API
|
||||||
|
getReferralTree: async (
|
||||||
|
accountSequence: string,
|
||||||
|
direction: 'up' | 'down' | 'both' = 'both',
|
||||||
|
depth: number = 1
|
||||||
|
): Promise<ReferralTreeData> => {
|
||||||
|
const response = await apiClient.get(`/users/${accountSequence}/referral-tree`, {
|
||||||
|
params: { direction, depth },
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlantingLedger: async (
|
||||||
|
accountSequence: string,
|
||||||
|
params: PaginationParams
|
||||||
|
): Promise<PlantingLedgerResponse> => {
|
||||||
|
const response = await apiClient.get(`/users/${accountSequence}/planting-ledger`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getWalletLedger: async (
|
||||||
|
accountSequence: string,
|
||||||
|
params: PaginationParams
|
||||||
|
): Promise<WalletLedgerResponse> => {
|
||||||
|
const response = await apiClient.get(`/users/${accountSequence}/wallet-ledger`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { usePlantingLedger } from '../hooks/use-users';
|
||||||
|
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
||||||
|
import { formatDateTime } from '@/lib/utils/date';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ChevronLeft, ChevronRight, TreePine, Calendar, DollarSign } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PlantingLedgerProps {
|
||||||
|
accountSequence: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认种状态标签
|
||||||
|
const plantingStatusLabels: Record<string, string> = {
|
||||||
|
CREATED: '已创建',
|
||||||
|
PAID: '已支付',
|
||||||
|
FUND_ALLOCATED: '资金已分配',
|
||||||
|
MINING_ENABLED: '已开始挖矿',
|
||||||
|
CANCELLED: '已取消',
|
||||||
|
EXPIRED: '已过期',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态对应的样式
|
||||||
|
const getStatusVariant = (status: string): 'default' | 'secondary' | 'destructive' | 'outline' => {
|
||||||
|
switch (status) {
|
||||||
|
case 'MINING_ENABLED':
|
||||||
|
case 'PAID':
|
||||||
|
case 'FUND_ALLOCATED':
|
||||||
|
return 'default';
|
||||||
|
case 'CREATED':
|
||||||
|
return 'secondary';
|
||||||
|
case 'CANCELLED':
|
||||||
|
case 'EXPIRED':
|
||||||
|
return 'destructive';
|
||||||
|
default:
|
||||||
|
return 'outline';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PlantingLedger({ accountSequence }: PlantingLedgerProps) {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const { data, isLoading } = usePlantingLedger(accountSequence, { page, pageSize });
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">认种汇总</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">认种信息</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground text-center py-8">暂无认种数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 认种汇总 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<TreePine className="h-5 w-5 text-green-600" />
|
||||||
|
认种汇总
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">总订单数</p>
|
||||||
|
<p className="text-2xl font-bold">{formatNumber(data.summary.totalOrders)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">总认种量</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600">{formatNumber(data.summary.totalTreeCount)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">总金额</p>
|
||||||
|
<p className="text-2xl font-bold text-primary">{formatDecimal(data.summary.totalAmount, 2)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">有效认种量</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-600">{formatNumber(data.summary.effectiveTreeCount)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">首次认种</p>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{data.summary.firstPlantingAt ? formatDateTime(data.summary.firstPlantingAt) : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">最近认种</p>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{data.summary.lastPlantingAt ? formatDateTime(data.summary.lastPlantingAt) : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 认种分类账明细 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">认种分类账明细</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>订单号</TableHead>
|
||||||
|
<TableHead className="text-right">认种数量</TableHead>
|
||||||
|
<TableHead className="text-right">金额</TableHead>
|
||||||
|
<TableHead>省市</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead>创建时间</TableHead>
|
||||||
|
<TableHead>支付时间</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.items.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||||
|
暂无认种记录
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
data.items.map((item) => (
|
||||||
|
<TableRow key={item.orderId}>
|
||||||
|
<TableCell className="font-mono text-sm">{item.orderNo}</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">{formatNumber(item.treeCount)}</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">{formatDecimal(item.totalAmount, 2)}</TableCell>
|
||||||
|
<TableCell className="text-sm">
|
||||||
|
{item.selectedProvince || '-'} / {item.selectedCity || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={getStatusVariant(item.status)}>
|
||||||
|
{plantingStatusLabels[item.status] || item.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{formatDateTime(item.createdAt)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{item.paidAt ? formatDateTime(item.paidAt) : '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{data.totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between p-4 border-t">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
共 {formatNumber(data.total)} 条,第 {page} / {data.totalPages} 页
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setPage(page - 1)} disabled={page <= 1}>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
disabled={page >= data.totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useReferralTree } from '../hooks/use-users';
|
||||||
|
import { usersApi } from '../api/users.api';
|
||||||
|
import { formatNumber } from '@/lib/utils/format';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { ChevronDown, ChevronRight, Users, TreePine, Building2 } from 'lucide-react';
|
||||||
|
import type { ReferralNode } from '@/types/user';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface ReferralTreeProps {
|
||||||
|
accountSequence: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReferralTree({ accountSequence }: ReferralTreeProps) {
|
||||||
|
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
|
||||||
|
const [expandedNodes, setExpandedNodes] = useState<Record<string, ReferralNode[] | null>>({});
|
||||||
|
|
||||||
|
const { data: referralTree, isLoading } = useReferralTree(treeRootUser, 'both', 1);
|
||||||
|
|
||||||
|
// 当 referralTree 数据加载完成后,自动展开当前用户的直推下级
|
||||||
|
useEffect(() => {
|
||||||
|
if (referralTree && referralTree.directReferrals.length > 0) {
|
||||||
|
setExpandedNodes((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[referralTree.currentUser.accountSequence]: referralTree.directReferrals,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [referralTree]);
|
||||||
|
|
||||||
|
// 切换推荐关系树的根节点
|
||||||
|
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 usersApi.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]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">引荐关系</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!referralTree) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">引荐关系</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground text-center py-8">暂无引荐关系数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle className="text-lg">引荐关系</CardTitle>
|
||||||
|
{treeRootUser !== accountSequence && (
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setTreeRootUser(accountSequence)}>
|
||||||
|
返回当前用户
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* 向上的引荐人链 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">引荐人链 (向上)</p>
|
||||||
|
{referralTree.ancestors.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{referralTree.ancestors.map((ancestor, index) => (
|
||||||
|
<div key={ancestor.accountSequence}>
|
||||||
|
<ReferralNodeCard
|
||||||
|
node={ancestor}
|
||||||
|
onClick={() => handleTreeNodeClick(ancestor)}
|
||||||
|
variant="ancestor"
|
||||||
|
/>
|
||||||
|
{index < referralTree.ancestors.length - 1 && (
|
||||||
|
<div className="flex justify-center py-1">
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-center py-1">
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center gap-2 p-3 bg-muted rounded-lg">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">总部</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当前用户 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">当前用户</p>
|
||||||
|
<ReferralNodeCard
|
||||||
|
node={referralTree.currentUser}
|
||||||
|
isCurrentUser
|
||||||
|
isHighlight={referralTree.currentUser.accountSequence === accountSequence}
|
||||||
|
expandedNodes={expandedNodes}
|
||||||
|
onToggle={handleToggleNode}
|
||||||
|
onClick={handleTreeNodeClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 直推下级列表 */}
|
||||||
|
{referralTree.directReferrals.length > 0 && (
|
||||||
|
<div className="space-y-2 ml-6 border-l-2 border-muted pl-4">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
|
直推下级 ({referralTree.directReferrals.length})
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{referralTree.directReferrals.map((child) => (
|
||||||
|
<ReferralNodeCard
|
||||||
|
key={child.accountSequence}
|
||||||
|
node={child}
|
||||||
|
expandedNodes={expandedNodes}
|
||||||
|
onToggle={handleToggleNode}
|
||||||
|
onClick={handleTreeNodeClick}
|
||||||
|
showExpandButton
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReferralNodeCardProps {
|
||||||
|
node: ReferralNode;
|
||||||
|
isCurrentUser?: boolean;
|
||||||
|
isHighlight?: boolean;
|
||||||
|
variant?: 'ancestor' | 'current' | 'child';
|
||||||
|
expandedNodes?: Record<string, ReferralNode[] | null>;
|
||||||
|
onToggle?: (nodeSeq: string, hasChildren: boolean) => void;
|
||||||
|
onClick?: (node: ReferralNode) => void;
|
||||||
|
showExpandButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReferralNodeCard({
|
||||||
|
node,
|
||||||
|
isCurrentUser = false,
|
||||||
|
isHighlight = false,
|
||||||
|
variant,
|
||||||
|
expandedNodes = {},
|
||||||
|
onToggle,
|
||||||
|
onClick,
|
||||||
|
showExpandButton = false,
|
||||||
|
}: ReferralNodeCardProps) {
|
||||||
|
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="space-y-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 rounded-lg border transition-colors cursor-pointer hover:bg-accent',
|
||||||
|
isCurrentUser && 'border-primary bg-primary/5',
|
||||||
|
isHighlight && 'ring-2 ring-primary'
|
||||||
|
)}
|
||||||
|
onClick={() => onClick?.(node)}
|
||||||
|
>
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarImage src={node.avatar || undefined} alt={node.nickname || ''} />
|
||||||
|
<AvatarFallback>{node.nickname?.charAt(0) || 'U'}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-sm">{node.accountSequence}</span>
|
||||||
|
<span className="text-sm truncate">{node.nickname || '未设置'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-1">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<TreePine className="h-3 w-3 text-green-600" />
|
||||||
|
个人: {formatNumber(node.personalAdoptions)}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users className="h-3 w-3 text-blue-600" />
|
||||||
|
团队: {formatNumber(node.teamAdoptions)}
|
||||||
|
</span>
|
||||||
|
{node.directReferralCount > 0 && (
|
||||||
|
<span>引荐: {formatNumber(node.directReferralCount)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showExpandButton && hasChildren && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggle?.(node.accountSequence, hasChildren);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="animate-spin">...</span>
|
||||||
|
) : isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/users/${node.accountSequence}`}
|
||||||
|
className="text-xs text-primary hover:underline"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 展开的子节点 */}
|
||||||
|
{isExpanded && children.length > 0 && (
|
||||||
|
<div className="ml-6 border-l-2 border-muted pl-4 space-y-2">
|
||||||
|
{children.map((child) => (
|
||||||
|
<ReferralNodeCard
|
||||||
|
key={child.accountSequence}
|
||||||
|
node={child}
|
||||||
|
expandedNodes={expandedNodes}
|
||||||
|
onToggle={onToggle}
|
||||||
|
onClick={onClick}
|
||||||
|
showExpandButton
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useWalletLedger } from '../hooks/use-users';
|
||||||
|
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
||||||
|
import { formatDateTime } from '@/lib/utils/date';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { ChevronLeft, ChevronRight, Wallet, TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface WalletLedgerProps {
|
||||||
|
accountSequence: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流水类型标签
|
||||||
|
const entryTypeLabels: Record<string, string> = {
|
||||||
|
DEPOSIT: '充值',
|
||||||
|
DEPOSIT_USDT: '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: '手续费',
|
||||||
|
MINING_REWARD: '挖矿奖励',
|
||||||
|
TRADE_BUY: '买入',
|
||||||
|
TRADE_SELL: '卖出',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 资产类型标签
|
||||||
|
const assetTypeLabels: Record<string, string> = {
|
||||||
|
USDT: '绿积分',
|
||||||
|
DST: 'DST',
|
||||||
|
BNB: 'BNB',
|
||||||
|
OG: 'OG',
|
||||||
|
RWAD: 'RWAD',
|
||||||
|
HASHPOWER: '算力',
|
||||||
|
MINING: '挖矿积分',
|
||||||
|
TRADING: '交易积分',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WalletLedger({ accountSequence }: WalletLedgerProps) {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const { data, isLoading } = useWalletLedger(accountSequence, { page, pageSize });
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">钱包汇总</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">钱包信息</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground text-center py-8">暂无钱包数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 钱包汇总 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Wallet className="h-5 w-5 text-blue-600" />
|
||||||
|
钱包汇总
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">绿积分 可用</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600 font-mono">
|
||||||
|
{formatDecimal(data.summary.usdtAvailable, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">绿积分 冻结</p>
|
||||||
|
<p className="text-2xl font-bold text-orange-600 font-mono">
|
||||||
|
{formatDecimal(data.summary.usdtFrozen, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">待领取收益</p>
|
||||||
|
<p className="text-2xl font-bold text-yellow-600 font-mono">
|
||||||
|
{formatDecimal(data.summary.pendingUsdt, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">可结算收益</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-600 font-mono">
|
||||||
|
{formatDecimal(data.summary.settleableUsdt, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">已结算收益</p>
|
||||||
|
<p className="text-2xl font-bold text-primary font-mono">
|
||||||
|
{formatDecimal(data.summary.settledTotalUsdt, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">过期收益</p>
|
||||||
|
<p className="text-2xl font-bold text-muted-foreground font-mono">
|
||||||
|
{formatDecimal(data.summary.expiredTotalUsdt, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 钱包分类账明细 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">钱包分类账明细</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>流水ID</TableHead>
|
||||||
|
<TableHead>类型</TableHead>
|
||||||
|
<TableHead>资产</TableHead>
|
||||||
|
<TableHead className="text-right">金额</TableHead>
|
||||||
|
<TableHead className="text-right">余额快照</TableHead>
|
||||||
|
<TableHead>关联订单</TableHead>
|
||||||
|
<TableHead>备注</TableHead>
|
||||||
|
<TableHead>时间</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.items.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||||
|
暂无钱包流水
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
data.items.map((item) => {
|
||||||
|
const amount = parseFloat(item.amount);
|
||||||
|
const isPositive = amount >= 0;
|
||||||
|
return (
|
||||||
|
<TableRow key={item.entryId}>
|
||||||
|
<TableCell className="font-mono text-xs">{item.entryId}</TableCell>
|
||||||
|
<TableCell className="text-sm">
|
||||||
|
{entryTypeLabels[item.entryType] || item.entryType}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm">
|
||||||
|
{assetTypeLabels[item.assetType] || item.assetType}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-mono flex items-center justify-end gap-1',
|
||||||
|
isPositive ? 'text-green-600' : 'text-red-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isPositive ? (
|
||||||
|
<TrendingUp className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
{isPositive ? '+' : ''}
|
||||||
|
{formatDecimal(item.amount, 4)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-sm">
|
||||||
|
{formatDecimal(item.balanceAfter, 4)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs text-muted-foreground max-w-[120px] truncate">
|
||||||
|
{item.refOrderId || item.refTxHash || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm max-w-[100px] truncate" title={item.memo}>
|
||||||
|
{item.memo || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{formatDateTime(item.createdAt)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between p-4 border-t">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
共 {formatNumber(data.total)} 条,第 {page} / {data.totalPages} 页
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setPage(page - 1)} disabled={page <= 1}>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
disabled={page >= data.totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -40,3 +40,32 @@ export function useTradeOrders(accountSequence: string, params: PaginationParams
|
||||||
enabled: !!accountSequence,
|
enabled: !!accountSequence,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 admin-web 复用的 hooks
|
||||||
|
export function useReferralTree(
|
||||||
|
accountSequence: string,
|
||||||
|
direction: 'up' | 'down' | 'both' = 'both',
|
||||||
|
depth: number = 1
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['users', accountSequence, 'referral-tree', direction, depth],
|
||||||
|
queryFn: () => usersApi.getReferralTree(accountSequence, direction, depth),
|
||||||
|
enabled: !!accountSequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlantingLedger(accountSequence: string, params: PaginationParams) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['users', accountSequence, 'planting-ledger', params],
|
||||||
|
queryFn: () => usersApi.getPlantingLedger(accountSequence, params),
|
||||||
|
enabled: !!accountSequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWalletLedger(accountSequence: string, params: PaginationParams) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['users', accountSequence, 'wallet-ledger', params],
|
||||||
|
queryFn: () => usersApi.getWalletLedger(accountSequence, params),
|
||||||
|
enabled: !!accountSequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,32 @@
|
||||||
export interface UserOverview {
|
export interface UserOverview {
|
||||||
accountSequence: number;
|
accountSequence: number;
|
||||||
|
accountId?: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
phoneNumberMasked?: string;
|
||||||
|
avatar?: string | null;
|
||||||
hasAdopted: boolean;
|
hasAdopted: boolean;
|
||||||
totalContribution: string;
|
totalContribution: string;
|
||||||
effectiveContribution: string;
|
effectiveContribution: string;
|
||||||
miningBalance: string;
|
miningBalance: string;
|
||||||
tradingBalance: string;
|
tradingBalance: string;
|
||||||
frozenBalance: string;
|
frozenBalance: string;
|
||||||
|
// 从 admin-web 复用的字段
|
||||||
|
personalAdoptions?: number;
|
||||||
|
teamAdoptions?: number;
|
||||||
|
teamAddresses?: number;
|
||||||
|
provincialAdoptions?: {
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
cityAdoptions?: {
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
referrerId?: string | null;
|
||||||
|
ranking?: number | null;
|
||||||
|
status?: 'active' | 'frozen' | 'deactivated';
|
||||||
|
isOnline?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,6 +37,17 @@ export interface UserDetail extends UserOverview {
|
||||||
teamSize: number;
|
teamSize: number;
|
||||||
teamAdoptedCount: number;
|
teamAdoptedCount: number;
|
||||||
contributions: ContributionBreakdown;
|
contributions: ContributionBreakdown;
|
||||||
|
// 从 admin-web 复用的字段
|
||||||
|
kycStatus?: string;
|
||||||
|
registeredAt?: string;
|
||||||
|
lastActiveAt?: string;
|
||||||
|
referralInfo?: {
|
||||||
|
referrerSequence?: string;
|
||||||
|
referrerNickname?: string;
|
||||||
|
usedReferralCode?: string;
|
||||||
|
depth?: number;
|
||||||
|
directReferralCount?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContributionBreakdown {
|
export interface ContributionBreakdown {
|
||||||
|
|
@ -65,3 +95,87 @@ export interface TradeOrder {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 引荐关系节点
|
||||||
|
export interface ReferralNode {
|
||||||
|
accountSequence: string;
|
||||||
|
nickname: string | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
personalAdoptions: number;
|
||||||
|
teamAdoptions: number;
|
||||||
|
directReferralCount: number;
|
||||||
|
isOnline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引荐关系树
|
||||||
|
export interface ReferralTreeData {
|
||||||
|
currentUser: ReferralNode;
|
||||||
|
ancestors: ReferralNode[];
|
||||||
|
directReferrals: ReferralNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认种订单
|
||||||
|
export interface PlantingOrder {
|
||||||
|
orderId: string;
|
||||||
|
orderNo: string;
|
||||||
|
treeCount: number;
|
||||||
|
totalAmount: string;
|
||||||
|
selectedProvince?: string;
|
||||||
|
selectedCity?: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
paidAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认种汇总
|
||||||
|
export interface PlantingSummary {
|
||||||
|
totalOrders: number;
|
||||||
|
totalTreeCount: number;
|
||||||
|
totalAmount: string;
|
||||||
|
effectiveTreeCount: number;
|
||||||
|
firstPlantingAt?: string;
|
||||||
|
lastPlantingAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认种分类账响应
|
||||||
|
export interface PlantingLedgerResponse {
|
||||||
|
summary: PlantingSummary;
|
||||||
|
items: PlantingOrder[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钱包流水
|
||||||
|
export interface WalletLedgerItem {
|
||||||
|
entryId: string;
|
||||||
|
entryType: string;
|
||||||
|
assetType: string;
|
||||||
|
amount: string;
|
||||||
|
balanceAfter: string;
|
||||||
|
refOrderId?: string;
|
||||||
|
refTxHash?: string;
|
||||||
|
memo?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钱包汇总
|
||||||
|
export interface WalletSummary {
|
||||||
|
usdtAvailable: string;
|
||||||
|
usdtFrozen: string;
|
||||||
|
pendingUsdt: string;
|
||||||
|
settleableUsdt: string;
|
||||||
|
settledTotalUsdt: string;
|
||||||
|
expiredTotalUsdt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钱包分类账响应
|
||||||
|
export interface WalletLedgerResponse {
|
||||||
|
summary: WalletSummary;
|
||||||
|
items: WalletLedgerItem[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue