206 lines
7.2 KiB
TypeScript
206 lines
7.2 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback } from 'react';
|
|
import { useParams, useRouter } from 'next/navigation';
|
|
import { useUser, useUpdateUser } from '@/hooks/use-users';
|
|
import { User } from '@/lib/user-api';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
ArrowLeft,
|
|
Pencil,
|
|
Shield,
|
|
Ban,
|
|
CheckCircle,
|
|
} from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
|
|
type UserStatus = 'active' | 'inactive' | 'locked';
|
|
|
|
function getUserStatus(user: User): UserStatus {
|
|
if (!user.isActive) return 'inactive';
|
|
return 'active';
|
|
}
|
|
|
|
function StatusBadge({ status }: { status: UserStatus }) {
|
|
const config: Record<UserStatus, { label: string; className: string }> = {
|
|
active: { label: '活跃', className: 'bg-green-100 text-green-700 hover:bg-green-100' },
|
|
inactive: { label: '禁用', className: 'bg-gray-100 text-gray-700 hover:bg-gray-100' },
|
|
locked: { label: '锁定', className: 'bg-red-100 text-red-700 hover:bg-red-100' },
|
|
};
|
|
const { label, className } = config[status];
|
|
return <Badge variant="secondary" className={className}>{label}</Badge>;
|
|
}
|
|
|
|
function DetailSkeleton() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<Skeleton className="h-16 w-16 rounded-full" />
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-6 w-32" />
|
|
<Skeleton className="h-4 w-20" />
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="space-y-2">
|
|
<Skeleton className="h-4 w-16" />
|
|
<Skeleton className="h-5 w-32" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function UserDetailPage() {
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const userId = params.id as string;
|
|
|
|
const { data, isLoading } = useUser(userId);
|
|
const updateUser = useUpdateUser();
|
|
|
|
const user = (data?.data?.data as User) ?? null;
|
|
|
|
const handleToggleActive = useCallback(() => {
|
|
if (!user) return;
|
|
const action = user.isActive ? '禁用' : '启用';
|
|
updateUser.mutate(
|
|
{ id: user.id, data: { isActive: !user.isActive } },
|
|
{
|
|
onSuccess: () => toast.success(`已${action}用户 ${user.username}`),
|
|
onError: () => toast.error(`${action}用户失败,请重试`),
|
|
},
|
|
);
|
|
}, [user, updateUser]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="icon" onClick={() => router.push('/admin/users')}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Skeleton className="h-8 w-32" />
|
|
</div>
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<DetailSkeleton />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="icon" onClick={() => router.push('/admin/users')}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<h1 className="text-2xl font-bold">用户详情</h1>
|
|
</div>
|
|
<Card>
|
|
<CardContent className="p-6 text-center text-muted-foreground">
|
|
用户不存在或已被删除
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="icon" onClick={() => router.push('/admin/users')}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<h1 className="text-2xl font-bold">用户详情</h1>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Avatar className="h-16 w-16">
|
|
{user.avatar && <AvatarImage src={user.avatar} alt={user.username} />}
|
|
<AvatarFallback className="text-xl">
|
|
{user.firstName?.[0] || user.username[0]}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<CardTitle className="text-xl">{user.username}</CardTitle>
|
|
<div className="mt-1">
|
|
<StatusBadge status={getUserStatus(user)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" onClick={() => router.push('/admin/users')}>
|
|
<Pencil className="mr-2 h-4 w-4" />
|
|
编辑
|
|
</Button>
|
|
<Button variant="outline" onClick={handleToggleActive}>
|
|
{user.isActive ? (
|
|
<><Ban className="mr-2 h-4 w-4" />禁用</>
|
|
) : (
|
|
<><CheckCircle className="mr-2 h-4 w-4" />启用</>
|
|
)}
|
|
</Button>
|
|
<Button variant="outline" onClick={() => router.push(`/admin/users/${user.id}/permissions`)}>
|
|
<Shield className="mr-2 h-4 w-4" />
|
|
分配角色
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<Separator />
|
|
<CardContent className="pt-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">用户名</p>
|
|
<p className="font-medium">{user.username}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">手机号</p>
|
|
<p className="font-medium">{user.phone || '-'}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">邮箱</p>
|
|
<p className="font-medium">{user.email || '-'}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">状态</p>
|
|
<StatusBadge status={getUserStatus(user)} />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">角色</p>
|
|
<p className="font-medium">-</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">创建时间</p>
|
|
<p className="font-medium">
|
|
{new Date(user.createdAt).toLocaleString('zh-CN')}
|
|
</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">最后登录时间</p>
|
|
<p className="font-medium">
|
|
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString('zh-CN') : '-'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|