Files
book-comic-library/frontend/src/pages/admin/Users.tsx
T

114 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState, type FormEvent } from "react";
import { api } from "../../api/client";
import { useAuth } from "../../auth/AuthContext";
import { toast } from "../../components/Toaster";
import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton";
export default function AdminUsers() {
const qc = useQueryClient();
const { user: me } = useAuth();
const usersQ = useQuery({ queryKey: ["users"], queryFn: api.listUsers });
const [u, setU] = useState("");
const [p, setP] = useState("");
const [role, setRole] = useState<"member" | "admin">("member");
const create = useMutation({
mutationFn: () => api.createUser(u.trim(), p, role),
onSuccess: () => {
toast("ok", "用户已创建");
setU("");
setP("");
void qc.invalidateQueries({ queryKey: ["users"] });
},
onError: (e) => toast("err", e instanceof Error ? e.message : "创建失败"),
});
const del = useMutation({
mutationFn: (id: number) => api.deleteUser(id),
onSuccess: () => {
toast("ok", "用户已删除(其阅读进度一并删除)");
void qc.invalidateQueries({ queryKey: ["users"] });
},
onError: (e) => toast("err", e instanceof Error ? e.message : "删除失败"),
});
function submit(e: FormEvent) {
e.preventDefault();
create.mutate();
}
return (
<div className="mx-auto w-full max-w-3xl px-4 py-6 sm:px-6">
<h1 className="mb-4 text-lg font-semibold tracking-tight">用户管理</h1>
<form onSubmit={submit} className="mb-6 flex flex-wrap items-center gap-3">
<Input className="max-w-40" placeholder="用户名" value={u} onChange={(e) => setU(e.target.value)} />
<Input className="max-w-48" type="password" placeholder="密码(≥8 位)" value={p} onChange={(e) => setP(e.target.value)} />
<Select value={role} onValueChange={(v) => setRole(v as "member" | "admin")}>
<SelectTrigger className="w-28"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="member">member</SelectItem>
<SelectItem value="admin">admin</SelectItem>
</SelectContent>
</Select>
<Button type="submit" disabled={create.isPending || !u.trim() || p.length < 8}>
{create.isPending ? "创建中…" : "创建"}
</Button>
</form>
{usersQ.isPending ? (
<Skeleton className="h-40 w-full" />
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16">ID</TableHead>
<TableHead>用户名</TableHead>
<TableHead>角色</TableHead>
<TableHead>创建时间</TableHead>
<TableHead className="w-16" />
</TableRow>
</TableHeader>
<TableBody>
{(usersQ.data ?? []).map((x) => (
<TableRow key={x.id}>
<TableCell className="text-muted-foreground">{x.id}</TableCell>
<TableCell>{x.username}</TableCell>
<TableCell>{x.role}</TableCell>
<TableCell className="text-muted-foreground">{x.created_at.slice(0, 19).replace("T", " ")}</TableCell>
<TableCell className="text-right">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive" disabled={x.id === me?.id} title={x.id === me?.id ? "不能删除自己" : undefined}>
删除
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>删除用户 {x.username}?</AlertDialogTitle>
<AlertDialogDescription>该用户的阅读进度与书签一并删除。</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-primary-foreground hover:bg-destructive/90" onClick={() => del.mutate(x.id)}>
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
);
}