mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 02:06:37 +08:00
prettier
This commit is contained in:
@@ -1,82 +1,93 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from "react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
|
||||
import {Eye, Layers, Plus, Pencil, UserRound, UserX,} from "lucide-react"
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Eye, Layers, Pencil, Plus, UserRound, UserX } from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
import type {AdminUser} from "@/lib/services/admin"
|
||||
import {formatDateTime} from "@/lib/utils"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {useAdminUsers} from "@/contexts/admin-users-context"
|
||||
import {CreateUserModal} from "./components/create-user-modal"
|
||||
import {UserFilterBar} from "./components/user-filter-bar"
|
||||
import {UserDetailSheet} from "./components/user-detail-sheet"
|
||||
import {EditUserModal} from "./components/edit-user-modal"
|
||||
import type { AdminUser } from '@/lib/services/admin';
|
||||
import { formatDateTime } from '@/lib/utils';
|
||||
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
import { ErrorInline } from '@/components/layout/error';
|
||||
import { useAdminUsers } from '@/contexts/admin-users-context';
|
||||
import { CreateUserModal } from './components/create-user-modal';
|
||||
import { UserFilterBar } from './components/user-filter-bar';
|
||||
import { UserDetailSheet } from './components/user-detail-sheet';
|
||||
import { EditUserModal } from './components/edit-user-modal';
|
||||
|
||||
export default function UsersPage() {
|
||||
const {
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
fetchUsers,
|
||||
getUserDetail,
|
||||
updateUserStatus
|
||||
} = useAdminUsers()
|
||||
const { users, loading, error, fetchUsers, getUserDetail, updateUserStatus } =
|
||||
useAdminUsers();
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const [detailLoading, setDetailLoading] = useState(false)
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||
const [editTarget, setEditTarget] = useState<AdminUser | null>(null)
|
||||
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editTarget, setEditTarget] = useState<AdminUser | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [fetchUsers])
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
|
||||
const handleStatusToggle = async (user: AdminUser) => {
|
||||
await updateUserStatus(user)
|
||||
await updateUserStatus(user);
|
||||
|
||||
if (selectedUser?.id === user.id) {
|
||||
setSelectedUser(prev => prev ? { ...prev, is_active: !prev.is_active } : null)
|
||||
setSelectedUser((prev) =>
|
||||
prev ? { ...prev, is_active: !prev.is_active } : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowDetail = async (user: AdminUser) => {
|
||||
setSelectedUser(user)
|
||||
setDetailOpen(true)
|
||||
setDetailLoading(true)
|
||||
setSelectedUser(user);
|
||||
setDetailOpen(true);
|
||||
setDetailLoading(true);
|
||||
|
||||
try {
|
||||
const detail = await getUserDetail(user.id)
|
||||
setSelectedUser(detail)
|
||||
const detail = await getUserDetail(user.id);
|
||||
setSelectedUser(detail);
|
||||
} catch {
|
||||
setSelectedUser(user)
|
||||
setSelectedUser(user);
|
||||
} finally {
|
||||
setDetailLoading(false)
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-6 space-y-4">
|
||||
<div className='py-6 space-y-4'>
|
||||
{/* 顶部标题栏 */}
|
||||
<div className="flex items-center justify-between pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserRound className="size-5 text-primary" />
|
||||
<div className='flex items-center justify-between pb-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<UserRound className='size-5 text-primary' />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">用户管理</h1>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>用户管理</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" className="h-7 text-xs" onClick={() => setCreateModalOpen(true)}>
|
||||
<Plus className="size-3.5 mr-1" />
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
className='h-7 text-xs'
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
<Plus className='size-3.5 mr-1' />
|
||||
新增用户
|
||||
</Button>
|
||||
</div>
|
||||
@@ -85,75 +96,108 @@ export default function UsersPage() {
|
||||
<UserFilterBar />
|
||||
|
||||
{error ? (
|
||||
<div className="p-8 border border-dashed rounded-lg">
|
||||
<ErrorInline error={error} onRetry={() => fetchUsers(true)} className="justify-center" />
|
||||
<div className='p-8 border border-dashed rounded-lg'>
|
||||
<ErrorInline
|
||||
error={error}
|
||||
onRetry={() => fetchUsers(true)}
|
||||
className='justify-center'
|
||||
/>
|
||||
</div>
|
||||
) : loading && users.length === 0 ? (
|
||||
<LoadingStateWithBorder icon={Layers} description="加载用户列表中..." />
|
||||
<LoadingStateWithBorder icon={Layers} description='加载用户列表中...' />
|
||||
) : users.length === 0 ? (
|
||||
<EmptyStateWithBorder icon={UserX} description="暂无用户数据" />
|
||||
<EmptyStateWithBorder icon={UserX} description='暂无用户数据' />
|
||||
) : (
|
||||
<div className="border border-dashed shadow-none rounded-lg overflow-hidden">
|
||||
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Table className="w-full caption-bottom text-sm min-w-full">
|
||||
<TableHeader className="sticky top-0 z-20 bg-background">
|
||||
<TableRow className="border-b border-dashed hover:bg-transparent">
|
||||
<TableHead className="w-[90px] whitespace-nowrap py-2 h-8">ID</TableHead>
|
||||
<TableHead className="w-[120px] whitespace-nowrap py-2 h-8">用户</TableHead>
|
||||
<TableHead className="whitespace-nowrap min-w-[160px] py-2 h-8 pl-4">邮箱</TableHead>
|
||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">上次登陆</TableHead>
|
||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">注册时间</TableHead>
|
||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">上次更新</TableHead>
|
||||
<TableHead className="sticky right-0 text-center bg-background z-10 w-[110px] py-2 h-8">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
className="border-dashed hover:bg-muted/30 cursor-pointer group"
|
||||
onClick={() => handleShowDetail(user)}
|
||||
>
|
||||
<TableCell className="font-mono text-[11px] text-muted-foreground py-1">{user.id}</TableCell>
|
||||
<TableCell className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-7 w-7 rounded-sm border">
|
||||
<AvatarImage src={user.avatar_url} />
|
||||
<AvatarFallback className="rounded-sm text-[10px]">
|
||||
{user.username.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-[11px] leading-tight max-w-[100px] truncate" title={user.nickname}>{user.nickname}</span>
|
||||
{user.is_admin && (
|
||||
<Badge variant="secondary" className="text-[9px] h-3.5 px-0.5 rounded-[2px] font-normal leading-none tracking-tighter">
|
||||
ADM
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted-foreground font-mono leading-tight">@{user.username}</span>
|
||||
<Table className='w-full caption-bottom text-sm min-w-full'>
|
||||
<TableHeader className='sticky top-0 z-20 bg-background'>
|
||||
<TableRow className='border-b border-dashed hover:bg-transparent'>
|
||||
<TableHead className='w-[90px] whitespace-nowrap py-2 h-8'>
|
||||
ID
|
||||
</TableHead>
|
||||
<TableHead className='w-[120px] whitespace-nowrap py-2 h-8'>
|
||||
用户
|
||||
</TableHead>
|
||||
<TableHead className='whitespace-nowrap min-w-[160px] py-2 h-8 pl-4'>
|
||||
邮箱
|
||||
</TableHead>
|
||||
<TableHead className='whitespace-nowrap min-w-[140px] py-2 h-8'>
|
||||
上次登陆
|
||||
</TableHead>
|
||||
<TableHead className='whitespace-nowrap min-w-[140px] py-2 h-8'>
|
||||
注册时间
|
||||
</TableHead>
|
||||
<TableHead className='whitespace-nowrap min-w-[140px] py-2 h-8'>
|
||||
上次更新
|
||||
</TableHead>
|
||||
<TableHead className='sticky right-0 text-center bg-background z-10 w-[110px] py-2 h-8'>
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
className='border-dashed hover:bg-muted/30 cursor-pointer group'
|
||||
onClick={() => handleShowDetail(user)}
|
||||
>
|
||||
<TableCell className='font-mono text-[11px] text-muted-foreground py-1'>
|
||||
{user.id}
|
||||
</TableCell>
|
||||
<TableCell className='py-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Avatar className='h-7 w-7 rounded-sm border'>
|
||||
<AvatarImage src={user.avatar_url} />
|
||||
<AvatarFallback className='rounded-sm text-[10px]'>
|
||||
{user.username.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='flex flex-col gap-0'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span
|
||||
className='font-medium text-[11px] leading-tight max-w-[100px] truncate'
|
||||
title={user.nickname}
|
||||
>
|
||||
{user.nickname}
|
||||
</span>
|
||||
{user.is_admin && (
|
||||
<Badge
|
||||
variant='secondary'
|
||||
className='text-[9px] h-3.5 px-0.5 rounded-[2px] font-normal leading-none tracking-tighter'
|
||||
>
|
||||
ADM
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='text-[10px] text-muted-foreground font-mono leading-tight'>
|
||||
@{user.username}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1 pl-4">
|
||||
{user.email || "-"}
|
||||
</TableCell>
|
||||
<TableCell className='text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1 pl-4'>
|
||||
{user.email || '-'}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1 pl-4">
|
||||
{formatDateTime(user.last_login_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1">
|
||||
{formatDateTime(user.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1">
|
||||
{formatDateTime(user.updated_at)}
|
||||
</TableCell>
|
||||
<TableCell className="sticky right-0 text-center bg-background z-10 py-1" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<TableCell className='text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1 pl-4'>
|
||||
{formatDateTime(user.last_login_at)}
|
||||
</TableCell>
|
||||
<TableCell className='text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1'>
|
||||
{formatDateTime(user.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className='text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1'>
|
||||
{formatDateTime(user.updated_at)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className='sticky right-0 text-center bg-background z-10 py-1'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='flex items-center justify-center gap-0.5'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
@@ -161,44 +205,56 @@ export default function UsersPage() {
|
||||
checked={user.is_active}
|
||||
onCheckedChange={() => handleStatusToggle(user)}
|
||||
disabled={user.is_admin}
|
||||
className="scale-75"
|
||||
className='scale-75'
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
{user.is_admin ? '管理员账户' : user.is_active ? '禁用账户' : '启用账户'}
|
||||
<TooltipContent side='top' className='text-xs'>
|
||||
{user.is_admin
|
||||
? '管理员账户'
|
||||
: user.is_active
|
||||
? '禁用账户'
|
||||
: '启用账户'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 text-muted-foreground hover:text-foreground" onClick={() => handleShowDetail(user)}>
|
||||
<Eye className="size-3" />
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-6 w-6 text-muted-foreground hover:text-foreground'
|
||||
onClick={() => handleShowDetail(user)}
|
||||
>
|
||||
<Eye className='size-3' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
<TooltipContent side='top' className='text-xs'>
|
||||
查看详情
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 text-muted-foreground hover:text-foreground" onClick={() => setEditTarget(user)}>
|
||||
<Pencil className="size-3" />
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-6 w-6 text-muted-foreground hover:text-foreground'
|
||||
onClick={() => setEditTarget(user)}
|
||||
>
|
||||
<Pencil className='size-3' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
<TooltipContent side='top' className='text-xs'>
|
||||
编辑用户
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
@@ -219,10 +275,11 @@ export default function UsersPage() {
|
||||
onClose={() => setEditTarget(null)}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
{/* 新建用户模态弹窗 */}
|
||||
<CreateUserModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} />
|
||||
<CreateUserModal
|
||||
isOpen={createModalOpen}
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user