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';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { useUserDetail } from '@/features/users/hooks/use-users';
|
||||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
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 { MiningRecordsList } from '@/features/users/components/mining-records-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() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="用户详情" description={`账户序列: ${accountSequence}`} backLink="/users" />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 基本信息卡片 */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">基本信息</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="flex items-start gap-6 mb-6">
|
||||
<div className="relative">
|
||||
<Avatar className="h-20 w-20">
|
||||
<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 className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground">账户序列</p>
|
||||
<p className="font-mono font-medium">{user?.accountSequence}</p>
|
||||
<span className="text-muted-foreground">账户序列:</span>
|
||||
<span className="ml-2 font-mono font-medium">{user?.accountSequence}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">昵称</p>
|
||||
<p className="font-medium">{user?.nickname || '-'}</p>
|
||||
<span className="text-muted-foreground">手机号:</span>
|
||||
<span className="ml-2">{user?.phoneNumberMasked || user?.phone}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">手机号</p>
|
||||
<p className="font-medium">{user?.phone}</p>
|
||||
<span className="text-muted-foreground">KYC状态:</span>
|
||||
<span className="ml-2">{user?.kycStatus || '未认证'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<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'
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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?.hasAdopted ? '已认种' : '未认种'}
|
||||
</span>
|
||||
{user.referrerAccountSequence}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="font-medium">-</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">推荐人</p>
|
||||
<p className="font-mono font-medium">{user?.referrerAccountSequence || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">直推人数</p>
|
||||
<p className="font-medium">{formatNumber(user?.directReferralCount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">直推认种</p>
|
||||
<p className="font-medium">{formatNumber(user?.directReferralAdoptedCount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 算力构成卡片 */}
|
||||
<Card>
|
||||
<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>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
|
|
@ -128,45 +201,102 @@ export default function UserDetailPage() {
|
|||
<span className="font-mono">{formatDecimal(user?.contributions?.teamBonus, 4)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between pt-3 border-t font-medium">
|
||||
<span>有效算力</span>
|
||||
<span className="font-mono text-primary">{formatDecimal(user?.effectiveContribution, 4)}</span>
|
||||
<span>有效算力 (贡献值)</span>
|
||||
<span className="font-mono text-primary text-lg">{formatDecimal(user?.effectiveContribution, 4)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 余额卡片 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">挖矿余额</p>
|
||||
<p className="text-2xl font-bold font-mono text-primary">{formatDecimal(user?.miningBalance, 4)}</p>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">交易余额</p>
|
||||
<p className="text-2xl font-bold font-mono text-blue-600">{formatDecimal(user?.tradingBalance, 4)}</p>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">冻结余额</p>
|
||||
<p className="text-2xl font-bold font-mono text-orange-600">{formatDecimal(user?.frozenBalance, 4)}</p>
|
||||
<p className="text-3xl font-bold font-mono text-orange-600 mt-1">
|
||||
{formatDecimal(user?.frozenBalance, 4)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tab 区域 */}
|
||||
<Tabs defaultValue="contributions">
|
||||
<TabsList>
|
||||
<TabsTrigger value="contributions">算力记录</TabsTrigger>
|
||||
<TabsTrigger value="mining">挖矿记录</TabsTrigger>
|
||||
<TabsTrigger value="trading">交易订单</TabsTrigger>
|
||||
<TabsList className="grid w-full grid-cols-6">
|
||||
<TabsTrigger value="contributions" className="flex items-center gap-1">
|
||||
<Zap className="h-4 w-4" />
|
||||
<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>
|
||||
|
||||
<TabsContent value="contributions" className="mt-4">
|
||||
<ContributionRecordsList accountSequence={accountSequence} />
|
||||
</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">
|
||||
<MiningRecordsList accountSequence={accountSequence} />
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { useUsers } from '@/features/users/hooks/use-users';
|
||||
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 { Button } from '@/components/ui/button';
|
||||
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 { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export default function UsersPage() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="用户管理" description="查看和管理用户的算力、积分股等信息" />
|
||||
<PageHeader title="用户管理" description="查看和管理用户的算力、贡献值、认种等信息" />
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
|
|
@ -56,16 +73,20 @@ export default function UsersPage() {
|
|||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px]">头像</TableHead>
|
||||
<TableHead>账户序列</TableHead>
|
||||
<TableHead>昵称</TableHead>
|
||||
<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 className="text-right">交易余额</TableHead>
|
||||
<TableHead>推荐人</TableHead>
|
||||
<TableHead>注册时间</TableHead>
|
||||
<TableHead className="w-[80px]">操作</TableHead>
|
||||
</TableRow>
|
||||
|
|
@ -74,7 +95,7 @@ export default function UsersPage() {
|
|||
{isLoading ? (
|
||||
[...Array(10)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(9)].map((_, j) => (
|
||||
{[...Array(12)].map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
|
|
@ -83,17 +104,40 @@ export default function UsersPage() {
|
|||
))
|
||||
) : data?.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={12} className="text-center py-8 text-muted-foreground">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data?.items.map((user) => (
|
||||
<TableRow key={user.accountSequence}>
|
||||
<TableCell className="font-mono">{user.accountSequence}</TableCell>
|
||||
<TableCell>{user.nickname || '-'}</TableCell>
|
||||
<TableCell>{user.phone}</TableCell>
|
||||
{/* 头像 */}
|
||||
<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'
|
||||
|
|
@ -102,12 +146,41 @@ export default function UsersPage() {
|
|||
{user.hasAdopted ? '已认种' : '未认种'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{/* 个人认种 */}
|
||||
<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">{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 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">
|
||||
|
|
@ -120,6 +193,7 @@ export default function UsersPage() {
|
|||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{data && data.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between p-4 border-t">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
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';
|
||||
|
||||
export const usersApi = {
|
||||
|
|
@ -36,4 +45,32 @@ export const usersApi = {
|
|||
const response = await apiClient.get(`/users/${accountSequence}/orders`, { params });
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// 从 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 {
|
||||
accountSequence: number;
|
||||
accountId?: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
phoneNumberMasked?: string;
|
||||
avatar?: string | null;
|
||||
hasAdopted: boolean;
|
||||
totalContribution: string;
|
||||
effectiveContribution: string;
|
||||
miningBalance: string;
|
||||
tradingBalance: 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;
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +37,17 @@ export interface UserDetail extends UserOverview {
|
|||
teamSize: number;
|
||||
teamAdoptedCount: number;
|
||||
contributions: ContributionBreakdown;
|
||||
// 从 admin-web 复用的字段
|
||||
kycStatus?: string;
|
||||
registeredAt?: string;
|
||||
lastActiveAt?: string;
|
||||
referralInfo?: {
|
||||
referrerSequence?: string;
|
||||
referrerNickname?: string;
|
||||
usedReferralCode?: string;
|
||||
depth?: number;
|
||||
directReferralCount?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ContributionBreakdown {
|
||||
|
|
@ -65,3 +95,87 @@ export interface TradeOrder {
|
|||
createdAt: 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