rwadurian/frontend/mining-admin-web/src/app/(dashboard)/users/page.tsx

239 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

'use client';
import { useState } 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';
import { formatDateTime } from '@/lib/utils/date';
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, 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('');
const [searchKeyword, setSearchKeyword] = useState('');
const [page, setPage] = useState(1);
const pageSize = 20;
const { data, isLoading, error } = useUsers({ page, pageSize, keyword: searchKeyword });
const handleSearch = () => {
setSearchKeyword(keyword);
setPage(1);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
// 获取状态徽章样式
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="查看和管理用户的算力、贡献值、认种等信息" />
<Card>
<CardContent className="p-4">
<div className="flex gap-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索账户序列、昵称、手机号..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
className="pl-10"
/>
</div>
<Button onClick={handleSearch}></Button>
</div>
</CardContent>
</Card>
<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 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>
</TableHeader>
<TableBody>
{isLoading ? (
[...Array(10)].map((_, i) => (
<TableRow key={i}>
{[...Array(12)].map((_, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-full" />
</TableCell>
))}
</TableRow>
))
) : error ? (
<TableRow>
<TableCell colSpan={12} className="text-center py-8 text-red-500">
: {(error as Error)?.message || '请稍后重试'}
</TableCell>
</TableRow>
) : !data?.items || data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={12} className="text-center py-8 text-muted-foreground">
</TableCell>
</TableRow>
) : (
data.items.map((user) => (
<TableRow key={user.accountSequence}>
{/* 头像 */}
<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)}
{(user.personalAdoptionOrders ?? 0) > 0 && (
<span className="text-muted-foreground ml-1">
({user.personalAdoptionOrders})
</span>
)}
</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)}
{(user.teamAdoptionOrders ?? 0) > 0 && (
<span className="text-muted-foreground ml-1">
({user.teamAdoptionOrders})
</span>
)}
</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 || '-'}
</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?.items && 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>
);
}