114 lines
5.0 KiB
TypeScript
114 lines
5.0 KiB
TypeScript
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>
|
||
);
|
||
}
|