94 lines
3.6 KiB
TypeScript
94 lines
3.6 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 { TopBar } from "../../components/TopBar";
|
|
import { toast } from "../../components/Toaster";
|
|
import { btn, btnPrimary, card, input } from "../../components/ui";
|
|
|
|
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="flex h-full flex-col">
|
|
<TopBar />
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
<h1 className="mb-4 text-lg font-semibold">用户管理</h1>
|
|
<form onSubmit={submit} className={`${card} mb-6 flex max-w-2xl flex-wrap items-center gap-3 p-4`}>
|
|
<input className={input} placeholder="用户名" value={u} onChange={(e) => setU(e.target.value)} />
|
|
<input className={input} type="password" placeholder="密码(≥8 位)" value={p} onChange={(e) => setP(e.target.value)} />
|
|
<select className={input} value={role} onChange={(e) => setRole(e.target.value as "member" | "admin")}>
|
|
<option value="member">member</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
<button className={btnPrimary} disabled={create.isPending || !u.trim() || p.length < 8}>
|
|
{create.isPending ? "创建中…" : "创建"}
|
|
</button>
|
|
</form>
|
|
<table className="w-full max-w-2xl text-sm">
|
|
<thead className="text-left text-zinc-500">
|
|
<tr>
|
|
<th className="py-1">ID</th>
|
|
<th>用户名</th>
|
|
<th>角色</th>
|
|
<th>创建时间</th>
|
|
<th />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(usersQ.data ?? []).map((x) => (
|
|
<tr key={x.id} className="border-t border-zinc-800">
|
|
<td className="py-1 text-zinc-500">{x.id}</td>
|
|
<td>{x.username}</td>
|
|
<td>{x.role}</td>
|
|
<td className="text-zinc-500">{x.created_at.slice(0, 19).replace("T", " ")}</td>
|
|
<td className="py-1 text-right">
|
|
<button
|
|
className={btn + " text-red-300"}
|
|
disabled={x.id === me?.id}
|
|
title={x.id === me?.id ? "不能删除自己" : undefined}
|
|
onClick={() => {
|
|
if (confirm(`删除用户 ${x.username}?`)) del.mutate(x.id);
|
|
}}
|
|
>
|
|
删除
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|