feat(users): add user admin section and delegate login authorization to API

This commit is contained in:
2026-08-10 23:04:41 +08:00
parent 88039d9ebe
commit 04c12ad340
9 changed files with 6630 additions and 2500 deletions
+220
View File
@@ -0,0 +1,220 @@
import { useQuery } from '@tanstack/react-query'
import { CheckCircle2, Shield, XCircle } from 'lucide-react'
import { fetchUserDetail } from '@/api/modules/users'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
interface UserDetailSheetProps {
uid: string | null
onClose: () => void
onAssignRoles?: () => void
}
export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheetProps) {
const query = useQuery({
queryKey: ['users', uid],
queryFn: () => (uid ? fetchUserDetail(uid) : null),
enabled: uid !== null,
})
const user = query.data?.data
return (
<Sheet open={uid !== null} onOpenChange={(open) => !open && onClose()}>
<SheetContent className="sm:max-w-md overflow-y-auto">
<SheetHeader className="border-b pb-4">
<div className="flex items-center gap-3">
<Avatar className="size-12">
<AvatarImage src={user?.avatar ?? undefined} alt="" />
<AvatarFallback className="text-base font-semibold">
{(user?.display_name ?? user?.username ?? 'U').slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<SheetTitle className="truncate text-base font-semibold">
{user?.display_name ?? user?.username ?? '用户详情'}
</SheetTitle>
<SheetDescription className="truncate text-xs font-mono">
{user?.uid}
</SheetDescription>
</div>
</div>
</SheetHeader>
{query.isPending ? (
<div className="py-8 text-center text-sm text-muted-foreground"></div>
) : !user ? (
<div className="py-8 text-center text-sm text-muted-foreground"></div>
) : (
<div className="flex flex-col gap-6 py-4">
{/* Status overview */}
<div className="flex flex-wrap gap-2">
<Badge variant={user.status ? 'default' : 'destructive'}>
{user.status ? '正常启用' : '已禁用'}
</Badge>
{user.is_robot ? (
<Badge variant="secondary"></Badge>
) : (
<Badge variant="outline"></Badge>
)}
{user.sid_display && (
<Badge variant="outline" className="font-mono">
SID: {user.sid_display}
</Badge>
)}
</div>
{/* Account Info */}
<div className="grid gap-3">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
</h4>
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.username ?? '—'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.display_name ?? '—'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">SID</div>
<div className="font-mono text-xs">{user.sid ?? '—'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.expertise ?? '—'}</div>
</div>
</div>
</div>
{/* Contact & Verification */}
<div className="grid gap-3">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
</h4>
<div className="grid gap-2 rounded-lg border p-3 text-sm">
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.email ?? '—'}</div>
</div>
{user.email && (
<Badge variant={user.email_verified_at ? 'secondary' : 'outline'} className="gap-1">
{user.email_verified_at ? (
<>
<CheckCircle2 className="size-3 text-emerald-500" />
</>
) : (
<>
<XCircle className="size-3 text-amber-500" />
</>
)}
</Badge>
)}
</div>
<div className="flex items-center justify-between border-t pt-2">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.phone ?? '—'}</div>
</div>
{user.phone && (
<Badge variant={user.phone_verified_at ? 'secondary' : 'outline'} className="gap-1">
{user.phone_verified_at ? (
<>
<CheckCircle2 className="size-3 text-emerald-500" />
</>
) : (
<>
<XCircle className="size-3 text-amber-500" />
</>
)}
</Badge>
)}
</div>
</div>
</div>
{/* Locale & Region */}
<div className="grid gap-3">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
</h4>
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
<div>
<div className="text-xs text-muted-foreground">/</div>
<div className="font-medium">{user.country_code ?? '—'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.city ?? '—'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.timezone ?? '—'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="font-medium">{user.locale ?? '—'}</div>
</div>
</div>
</div>
{/* Roles */}
<div className="grid gap-3">
<div className="flex items-center justify-between">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
</h4>
{onAssignRoles && (
<Button variant="ghost" size="sm" onClick={onAssignRoles} className="h-6 text-xs gap-1">
<Shield className="size-3" />
</Button>
)}
</div>
<div className="flex flex-wrap gap-1.5 rounded-lg border p-3">
{user.roles && user.roles.length > 0 ? (
user.roles.map((role) => (
<Badge key={role} variant="secondary">
{role}
</Badge>
))
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
</div>
{/* Timestamps */}
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-xs text-muted-foreground">
<div>
<span></span>
<span>{new Date(user.created_at).toLocaleString('zh-CN')}</span>
</div>
{user.updated_at && (
<div>
<span></span>
<span>{new Date(user.updated_at).toLocaleString('zh-CN')}</span>
</div>
)}
</div>
</div>
)}
</SheetContent>
</Sheet>
)
}