完成修改密码删除用户

This commit is contained in:
2025-05-12 12:47:27 +08:00
parent fbc9a4f140
commit 805901767c
9 changed files with 331 additions and 14 deletions

View File

@@ -26,38 +26,66 @@ import {
AlertTitle, AlertTitle,
} from "@/components/ui/alert" } from "@/components/ui/alert"
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
export function UserInfoEditor({ export function UserInfoEditor({
onClose, onClose,
onUserUpdate, onUserUpdate,
onUserDelete,
userId, userId,
}: { }: {
onClose: () => void, onClose: () => void,
onUserUpdate: (user: User) => void, onUserUpdate: (user: User) => void,
onUserDelete: (userId: string) => void,
userId: string userId: string
}) { }) {
const { user, isLoading, error } = userId ? useUser(userId) : {}; const { user, isLoading, error } = useUser(userId);
const [saveLoading, setSaveLoading] = React.useState(false);
const handleSave = async (user: updateUser) => { const handleSave = async (user: updateUser) => {
try { try {
setSaveLoading(true);
const res = await AdminApi.user.update(userId, user); const res = await AdminApi.user.update(userId, user);
if (res) { if (res) {
toast.success("保存成功"); toast.success("保存成功");
onUserUpdate(res); onUserUpdate(res);
onClose();
} else { } else {
throw new Error(); throw new Error();
} }
} catch (error) { } catch (error) {
toast.error((error as Error).message || "保存失败"); toast.error((error as Error).message || "保存失败");
} finally {
setSaveLoading(false);
} }
} }
const handleRemove = async () => { const [removeLoading, setRemoveLoading] = React.useState(false);
const handleRemove = async (userId: string) => {
try {
setRemoveLoading(true);
await AdminApi.user.remove(userId);
toast.success("删除成功");
onUserDelete(userId);
onClose();
} catch (error) {
toast.error((error as Error).message || "删除失败");
} finally {
setRemoveLoading(false);
}
} }
const handleSetPassword = async () => { const [setPasswordLoading, setSetPasswordLoading] = React.useState(false);
const handleSetPassword = async (userId: string, password: string) => {
try {
setSetPasswordLoading(true);
await AdminApi.user.setPassword(userId, password);
toast.success("密码修改成功");
} catch (error) {
toast.error((error as Error).message || "密码修改失败");
} finally {
setSetPasswordLoading(false);
}
} }
return ( return (
@@ -112,9 +140,11 @@ export function UserInfoEditor({
function ProfileForm({ className, user, onSetPassword, onRemove, ...props }: function ProfileForm({ className, user, onSetPassword, onRemove, ...props }:
React.ComponentProps<"form"> & { React.ComponentProps<"form"> & {
user: User, user: User,
onSetPassword: () => Promise<void>, onSetPassword: (userId: string, password: string) => Promise<void>,
onRemove: () => Promise<void>, onRemove: (userId: string) => Promise<void>,
}) { }) {
const [newPassword, setNewPassword] = React.useState<string>("");
return ( return (
<form className={cn("grid items-start gap-4", className)} {...props}> <form className={cn("grid items-start gap-4", className)} {...props}>
<div className="grid gap-2"> <div className="grid gap-2">
@@ -138,8 +168,56 @@ function ProfileForm({ className, user, onSetPassword, onRemove, ...props }:
<Input id="phone" name="phone" defaultValue={user.phone} /> <Input id="phone" name="phone" defaultValue={user.phone} />
</div> </div>
<div className="w-full flex gap-5"> <div className="w-full flex gap-5">
<Button type="button" variant="secondary" className="flex-1" onClick={onSetPassword}></Button> <Dialog>
<Button type="button" variant="destructive" className="flex-1" onClick={onRemove}></Button> <DialogTrigger asChild>
<Button type="button" variant="secondary" className="flex-1" onClick={() => setNewPassword('')}></Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" >
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
6-32
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="password" className="text-right">
</Label>
<Input
id="password"
name="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" onClick={() => onSetPassword(user.userId, newPassword)}></Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="button" variant="destructive" className="flex-1"></Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>?</AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => onRemove(user.userId)}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
<Button type="submit"></Button> <Button type="submit"></Button>
</form> </form>

View File

@@ -30,6 +30,21 @@ export default function Page() {
) )
} }
const handleUserDelete = async (userId: string) => {
await mutate(
(data) => {
if (!data) return data;
return {
...data,
items: data.items.filter((user) => user.userId !== userId),
};
},
{
revalidate: false,
}
)
}
return ( return (
<> <>
<Table> <Table>
@@ -89,6 +104,7 @@ export default function Page() {
onClose={() => setEditorUserId('')} onClose={() => setEditorUserId('')}
userId={editorUserId} userId={editorUserId}
onUserUpdate={handleUserUpdate} onUserUpdate={handleUserUpdate}
onUserDelete={handleUserDelete}
/> />
</> </>
) )

View File

@@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -7,7 +7,6 @@ export function useUser(userId: string) {
['/api/admin/user', userId], ['/api/admin/user', userId],
() => AdminApi.user.get(userId), () => AdminApi.user.get(userId),
{ {
revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
revalidateIfStale: false, revalidateIfStale: false,
dedupingInterval: 0, dedupingInterval: 0,

View File

@@ -2,4 +2,5 @@ export * from './list';
export * from './get'; export * from './get';
export * from './create'; export * from './create';
export * from './update'; export * from './update';
export * from './set-password'; export * from './set-password';
export * from './remove';

View File

@@ -1,7 +1,7 @@
import fetcher from "../../fetcher"; import fetcher from "../../fetcher";
export async function remove(userId: string) { export async function remove(userId: string) {
return fetcher(`/admin/user/${userId}`, { return fetcher(`/api/admin/user/${userId}`, {
method: 'DELETE', method: 'DELETE',
}) })
} }

View File

@@ -1,7 +1,7 @@
import fetcher from "../../fetcher"; import fetcher from "../../fetcher";
export async function setPassword(userId: string, password: string) { export async function setPassword(userId: string, password: string) {
return fetcher(`/admin/user/${userId}/password`, { return fetcher(`/api/admin/user/${userId}/password`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
password, password,

View File

@@ -9,6 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-avatar": "^1.1.7", "@radix-ui/react-avatar": "^1.1.7",
"@radix-ui/react-collapsible": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dialog": "^1.1.11",

View File

@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@radix-ui/react-alert-dialog':
specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-avatar': '@radix-ui/react-avatar':
specifier: ^1.1.7 specifier: ^1.1.7
version: 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -415,6 +418,19 @@ packages:
'@radix-ui/primitive@1.1.2': '@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/react-alert-dialog@1.1.13':
resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-arrow@1.1.4': '@radix-ui/react-arrow@1.1.4':
resolution: {integrity: sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==} resolution: {integrity: sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==}
peerDependencies: peerDependencies:
@@ -524,6 +540,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-dialog@1.1.13':
resolution: {integrity: sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.1': '@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies: peerDependencies:
@@ -2759,6 +2788,20 @@ snapshots:
'@radix-ui/primitive@1.1.2': {} '@radix-ui/primitive@1.1.2': {}
'@radix-ui/react-alert-dialog@1.1.13(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-dialog': 1.1.13(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.2(@types/react@19.1.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-arrow@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@radix-ui/react-arrow@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2864,6 +2907,28 @@ snapshots:
'@types/react': 19.1.2 '@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2) '@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-dialog@1.1.13(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
aria-hidden: 1.2.4
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-remove-scroll: 2.6.3(@types/react@19.1.2)(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-direction@1.1.1(@types/react@19.1.2)(react@19.1.0)': '@radix-ui/react-direction@1.1.1(@types/react@19.1.2)(react@19.1.0)':
dependencies: dependencies:
react: 19.1.0 react: 19.1.0