mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
feat(admin): support user profile editing, password resetting, and email column with search
- Add UpdateUser API and logics supporting nickname, email, admin flag modification, and password reset. - Relocate user delete button and confirmation Alert into the EditUserModal. - Optimize admin Switch change to trigger instant API request with rollback support. - Fix missing email field in edit form initialization by fetching full profile metadata. - Render email column in users list and support email-based filtering in UserFilterBar. - Remove hardcoded styles and sizes from Switch components to follow global theme.
This commit is contained in:
@@ -327,7 +327,7 @@ export function EventsTab() {
|
||||
<Switch
|
||||
checked={event.enabled}
|
||||
onCheckedChange={() => toggleEventMutation.mutate(event.id)}
|
||||
className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7"
|
||||
className="scale-75"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="sticky right-0 text-center bg-background z-10 py-1" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -324,7 +324,7 @@ export function SettingsTab() {
|
||||
}
|
||||
})
|
||||
}}
|
||||
className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7"
|
||||
className="scale-75"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="sticky right-0 text-center bg-background z-10 py-1" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useState} from "react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Label} from "@/components/ui/label"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {useAdminUsers} from "@/contexts/admin-users-context"
|
||||
import {useAuth} from "@/components/providers/auth-provider"
|
||||
import type {AdminUser} from "@/lib/services/admin"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import {Loader2} from "lucide-react"
|
||||
|
||||
interface EditUserForm {
|
||||
nickname: string
|
||||
email: string
|
||||
is_admin: boolean
|
||||
password?: string
|
||||
}
|
||||
|
||||
export function EditUserModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
user,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
user: AdminUser | null
|
||||
}) {
|
||||
const { updateUser, deleteUser, getUserDetail } = useAdminUsers()
|
||||
const { user: currentUser } = useAuth()
|
||||
|
||||
const isSelf = currentUser && user && currentUser.id.toString() === user.id.toString()
|
||||
|
||||
const [form, setForm] = useState<EditUserForm>({
|
||||
nickname: "",
|
||||
email: "",
|
||||
is_admin: false,
|
||||
password: "",
|
||||
})
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
if (isOpen && user) {
|
||||
setForm({
|
||||
nickname: user.nickname || "",
|
||||
email: user.email || "",
|
||||
is_admin: user.is_admin || false,
|
||||
password: "",
|
||||
})
|
||||
setErrors({})
|
||||
|
||||
getUserDetail(user.id)
|
||||
.then((detail) => {
|
||||
if (active && detail) {
|
||||
setForm({
|
||||
nickname: detail.nickname || "",
|
||||
email: detail.email || "",
|
||||
is_admin: detail.is_admin || false,
|
||||
password: "",
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore fetching error
|
||||
})
|
||||
} else {
|
||||
setSaving(false)
|
||||
}
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [isOpen, user, getUserDetail])
|
||||
|
||||
const validate = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
if (!form.email.trim()) {
|
||||
newErrors.email = "邮箱不能为空"
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) {
|
||||
newErrors.email = "邮箱格式不正确"
|
||||
}
|
||||
|
||||
if (form.password && form.password.length < 8) {
|
||||
newErrors.password = "密码长度不能少于 8 位"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!user || !validate()) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
await updateUser(user.id, {
|
||||
nickname: form.nickname.trim() || undefined,
|
||||
email: form.email.trim(),
|
||||
is_admin: form.is_admin,
|
||||
password: form.password?.trim() || undefined,
|
||||
})
|
||||
onClose()
|
||||
} catch {
|
||||
// Errors are handled by context toast notifications
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!user) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await deleteUser(user)
|
||||
onClose()
|
||||
} catch {
|
||||
// Errors are handled by context toast notifications
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setShowDeleteConfirm(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑用户</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改用户档案、授予/撤销管理权限或重置其密码。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-4 pt-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="username" className="text-xs text-muted-foreground">用户名 (不可修改)</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={user.username}
|
||||
disabled
|
||||
className="bg-muted/50 cursor-not-allowed select-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="nickname">昵称 (选填)</Label>
|
||||
<Input
|
||||
id="nickname"
|
||||
value={form.nickname}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, nickname: e.target.value }))}
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))}
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-xs text-destructive">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="password">重置密码 (选填)</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
placeholder="留空表示保持当前密码,输入则重置 (至少 8 位)"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-xs text-destructive">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10">
|
||||
<div>
|
||||
<div className="font-medium text-sm">管理员权限</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{isSelf ? "不能撤销当前登录用户的管理员权限。" : "开启后此账号将拥有后台管理权限。"}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.is_admin}
|
||||
disabled={!!isSelf || saving}
|
||||
onCheckedChange={async (checked) => {
|
||||
setForm((prev) => ({ ...prev, is_admin: checked }))
|
||||
setSaving(true)
|
||||
try {
|
||||
await updateUser(user.id, {
|
||||
nickname: form.nickname.trim() || undefined,
|
||||
email: form.email.trim(),
|
||||
is_admin: checked,
|
||||
})
|
||||
} catch {
|
||||
setForm((prev) => ({ ...prev, is_admin: !checked }))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2 border-t mt-2">
|
||||
<div>
|
||||
{!user.is_admin && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={saving}
|
||||
>
|
||||
删除用户
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" type="button" onClick={onClose} disabled={saving}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving} variant="secondary">
|
||||
{saving ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除用户</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除用户 {user.nickname || user.username} 吗?该操作会移除用户账号,删除后无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={saving}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleDelete()
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving && <Loader2 className="size-3 animate-spin mr-1" />}
|
||||
确认删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {Globe, Loader2, Mail, MapPin, ShieldCheck, Smartphone, Trash2, UserCheck,} from "lucide-react"
|
||||
import {Globe, Loader2, Mail, MapPin, ShieldCheck, Smartphone, UserCheck,} from "lucide-react"
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
@@ -16,7 +16,6 @@ interface UserDetailSheetProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
detailLoading: boolean
|
||||
onStatusToggle: (user: AdminUser) => Promise<void>
|
||||
onDeleteTarget: (user: AdminUser) => void
|
||||
}
|
||||
|
||||
export function UserDetailSheet({
|
||||
@@ -25,7 +24,6 @@ export function UserDetailSheet({
|
||||
onOpenChange,
|
||||
detailLoading,
|
||||
onStatusToggle,
|
||||
onDeleteTarget,
|
||||
}: UserDetailSheetProps) {
|
||||
|
||||
const displayValue = (value?: string) => value && value.trim() ? value : "-"
|
||||
@@ -156,7 +154,7 @@ export function UserDetailSheet({
|
||||
</div>
|
||||
|
||||
{!selectedUser.is_admin && (
|
||||
<div className="p-4 border-t bg-background/80 backdrop-blur-md shrink-0 flex flex-col gap-2">
|
||||
<div className="p-4 border-t bg-background/80 backdrop-blur-md shrink-0">
|
||||
<Button
|
||||
variant={selectedUser.is_active ? "destructive" : "default"}
|
||||
className={cn(
|
||||
@@ -179,14 +177,6 @@ export function UserDetailSheet({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-9 text-xs font-medium"
|
||||
onClick={() => onDeleteTarget(selectedUser)}
|
||||
>
|
||||
<Trash2 className="size-3 mr-1" />
|
||||
删除用户
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,17 +18,19 @@ export function UserFilterBar() {
|
||||
pageSize,
|
||||
searchUserId,
|
||||
searchUsername,
|
||||
searchEmail,
|
||||
statusFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
setSearchUserId,
|
||||
setSearchUsername,
|
||||
setSearchEmail,
|
||||
setStatusFilter,
|
||||
fetchUsers
|
||||
} = useAdminUsers()
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const hasSearchFilter = Boolean(searchUserId || searchUsername)
|
||||
const hasSearchFilter = Boolean(searchUserId || searchUsername || searchEmail)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
|
||||
@@ -72,6 +74,12 @@ export function UserFilterBar() {
|
||||
value={searchUsername}
|
||||
onChange={(e) => setSearchUsername(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full h-7 px-2 text-xs border border-dashed rounded-md outline-none focus:border-primary bg-background"
|
||||
placeholder="输入邮箱..."
|
||||
value={searchEmail}
|
||||
onChange={(e) => setSearchEmail(e.target.value)}
|
||||
/>
|
||||
{hasSearchFilter && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -80,6 +88,7 @@ export function UserFilterBar() {
|
||||
onClick={() => {
|
||||
setSearchUserId("")
|
||||
setSearchUsername("")
|
||||
setSearchEmail("")
|
||||
}}
|
||||
>
|
||||
清除
|
||||
@@ -160,6 +169,7 @@ export function UserFilterBar() {
|
||||
onClick={() => {
|
||||
setSearchUserId("")
|
||||
setSearchUsername("")
|
||||
setSearchEmail("")
|
||||
setStatusFilter('all')
|
||||
}}
|
||||
className="h-5 px-2 lg:px-3 text-[11px] font-medium text-muted-foreground hover:text-foreground"
|
||||
|
||||
@@ -6,18 +6,8 @@ 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, Loader2, Plus, Trash2, UserRound, UserX,} from "lucide-react"
|
||||
import {Eye, Layers, Plus, Pencil, UserRound, UserX,} from "lucide-react"
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
import type {AdminUser} from "@/lib/services/admin"
|
||||
import {formatDateTime} from "@/lib/utils"
|
||||
@@ -28,6 +18,7 @@ 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 {
|
||||
@@ -36,16 +27,14 @@ export default function UsersPage() {
|
||||
error,
|
||||
fetchUsers,
|
||||
getUserDetail,
|
||||
updateUserStatus,
|
||||
deleteUser
|
||||
updateUserStatus
|
||||
} = useAdminUsers()
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const [detailLoading, setDetailLoading] = useState(false)
|
||||
const [deleteTarget, setDeleteTarget] = useState<AdminUser | null>(null)
|
||||
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||
const [editTarget, setEditTarget] = useState<AdminUser | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
@@ -74,21 +63,7 @@ export default function UsersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
if (!deleteTarget) return
|
||||
|
||||
setDeleteLoading(true)
|
||||
try {
|
||||
await deleteUser(deleteTarget)
|
||||
if (selectedUser?.id === deleteTarget.id) {
|
||||
setDetailOpen(false)
|
||||
setSelectedUser(null)
|
||||
}
|
||||
setDeleteTarget(null)
|
||||
} finally {
|
||||
setDeleteLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6 space-y-4">
|
||||
@@ -125,7 +100,8 @@ export default function UsersPage() {
|
||||
<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-[140px] py-2 h-8 pl-4">上次登陆</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>
|
||||
@@ -163,6 +139,10 @@ export default function UsersPage() {
|
||||
</div>
|
||||
</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>
|
||||
@@ -181,7 +161,7 @@ export default function UsersPage() {
|
||||
checked={user.is_active}
|
||||
onCheckedChange={() => handleStatusToggle(user)}
|
||||
disabled={user.is_admin}
|
||||
className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7"
|
||||
className="scale-75"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
@@ -201,23 +181,18 @@ export default function UsersPage() {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{!user.is_admin && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => setDeleteTarget(user)}
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
编辑用户
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -235,27 +210,16 @@ export default function UsersPage() {
|
||||
onOpenChange={setDetailOpen}
|
||||
detailLoading={detailLoading}
|
||||
onStatusToggle={handleStatusToggle}
|
||||
onDeleteTarget={setDeleteTarget}
|
||||
/>
|
||||
|
||||
{/* 删除确认警告弹窗 */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && !deleteLoading && setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除用户</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除用户 {deleteTarget?.nickname || deleteTarget?.username} 吗?该操作会移除用户账号,删除后无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteLoading}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteUser} disabled={deleteLoading}>
|
||||
{deleteLoading && <Loader2 className="size-3 animate-spin" />}
|
||||
确认删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* 编辑用户弹窗 */}
|
||||
<EditUserModal
|
||||
user={editTarget}
|
||||
isOpen={!!editTarget}
|
||||
onClose={() => setEditTarget(null)}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
{/* 新建用户模态弹窗 */}
|
||||
<CreateUserModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} />
|
||||
|
||||
Reference in New Issue
Block a user