239 lines
10 KiB
TypeScript
239 lines
10 KiB
TypeScript
'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>
|
||
);
|
||
}
|