feat(users): add user admin section and delegate login authorization to API
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user