feat(ui): admin pages on shadcn table/card/alert-dialog; drop TopBar, trim icons/ui.ts to reader-compat surface
This commit is contained in:
@@ -2,9 +2,16 @@ 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";
|
||||
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();
|
||||
@@ -39,55 +46,68 @@ export default function AdminUsers() {
|
||||
}
|
||||
|
||||
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-stone-500">
|
||||
<tr>
|
||||
<th className="py-1">ID</th>
|
||||
<th>用户名</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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) => (
|
||||
<tr key={x.id} className="border-t border-stone-800">
|
||||
<td className="py-1 text-stone-500">{x.id}</td>
|
||||
<td>{x.username}</td>
|
||||
<td>{x.role}</td>
|
||||
<td className="text-stone-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>
|
||||
<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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user