Files
massive-cloud/client/src/pages/Users.tsx

69 lines
7.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 { useState } from "react";
import { useLocation } from "wouter";
import { ArrowLeft, Search, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
const roleNames = { super_admin: "超级管理员", admin: "管理员", user: "普通用户" } as const;
type Role = keyof typeof roleNames;
export default function Users() {
const [, setLocation] = useLocation();
const utils = trpc.useUtils();
const [search, setSearch] = useState("");
const [draftSearch, setDraftSearch] = useState("");
const [form, setForm] = useState({ username: "", temporaryPassword: "", name: "", email: "", role: "user" as Role });
const list = trpc.users.list.useQuery(search ? { search } : {});
const refresh = () => utils.users.list.invalidate();
const onError = (error: { message: string }) => toast.error(error.message);
const create = trpc.users.create.useMutation({ onSuccess: () => { toast.success("用户已创建"); setForm({ username: "", temporaryPassword: "", name: "", email: "", role: "user" }); void refresh(); }, onError });
const update = trpc.users.update.useMutation({ onSuccess: () => { toast.success("用户信息已更新"); void refresh(); }, onError });
const setActive = trpc.users.setActive.useMutation({ onSuccess: () => { toast.success("账号状态已更新"); void refresh(); }, onError });
const resetPassword = trpc.users.resetPassword.useMutation({ onSuccess: () => toast.success("临时密码已重置,旧会话已失效"), onError });
const remove = trpc.users.delete.useMutation({ onSuccess: () => { toast.success("用户已永久删除"); void refresh(); }, onError });
return (
<div className="min-h-screen bg-background p-6 space-y-6">
<header className="flex items-center justify-between">
<div className="flex items-center gap-3"><Button variant="ghost" size="icon" onClick={() => setLocation("/")}><ArrowLeft className="w-4 h-4" /></Button><div><h1 className="text-xl font-semibold">用户管理</h1><p className="text-sm text-muted-foreground">创建账号、分配角色并管理登录状态</p></div></div>
</header>
<section className="industrial-panel p-5">
<h2 className="font-medium mb-4">新建用户</h2>
<form className="grid grid-cols-1 md:grid-cols-5 gap-3 items-end" onSubmit={event => { event.preventDefault(); create.mutate(form); }}>
<div className="space-y-1"><Label>用户名</Label><Input value={form.username} onChange={e => setForm(v => ({ ...v, username: e.target.value }))} minLength={3} required /></div>
<div className="space-y-1"><Label>姓名</Label><Input value={form.name} onChange={e => setForm(v => ({ ...v, name: e.target.value }))} required /></div>
<div className="space-y-1"><Label>邮箱(可选)</Label><Input type="email" value={form.email} onChange={e => setForm(v => ({ ...v, email: e.target.value }))} /></div>
<div className="space-y-1"><Label>临时密码</Label><Input type="password" minLength={8} value={form.temporaryPassword} onChange={e => setForm(v => ({ ...v, temporaryPassword: e.target.value }))} required /></div>
<div className="flex gap-2"><Select value={form.role} onValueChange={(role: Role) => setForm(v => ({ ...v, role }))}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="user">普通用户</SelectItem><SelectItem value="admin">管理员</SelectItem><SelectItem value="super_admin">超级管理员</SelectItem></SelectContent></Select><Button type="submit" disabled={create.isPending}>创建</Button></div>
</form>
</section>
<section className="industrial-panel p-5 space-y-4">
<form className="flex gap-2 max-w-md" onSubmit={event => { event.preventDefault(); setSearch(draftSearch.trim()); }}><Input placeholder="搜索用户名、姓名或邮箱" value={draftSearch} onChange={e => setDraftSearch(e.target.value)} /><Button type="submit" variant="secondary"><Search className="w-4 h-4 mr-1" />搜索</Button></form>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="border-b text-left text-muted-foreground"><th className="p-2">用户</th><th className="p-2">角色</th><th className="p-2">状态</th><th className="p-2">首次改密</th><th className="p-2">最后登录</th><th className="p-2 text-right">操作</th></tr></thead>
<tbody>{list.data?.map(user => (
<tr key={user.id} className="border-b border-border/60">
<td className="p-2"><div className="font-medium">{user.name || "未命名"}</div><div className="text-xs text-muted-foreground">{user.username}{user.email ? ` · ${user.email}` : ""}</div></td>
<td className="p-2"><Select value={user.role} onValueChange={(role: Role) => update.mutate({ id: user.id, role })}><SelectTrigger className="w-36"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="user">普通用户</SelectItem><SelectItem value="admin">管理员</SelectItem><SelectItem value="super_admin">超级管理员</SelectItem></SelectContent></Select></td>
<td className="p-2"><button className={user.isActive === 1 ? "text-green-400" : "text-muted-foreground"} onClick={() => setActive.mutate({ id: user.id, isActive: user.isActive !== 1 })}>{user.isActive === 1 ? "已启用" : "已禁用"}</button></td>
<td className="p-2">{user.mustChangePassword === 1 ? "是" : "否"}</td>
<td className="p-2 text-xs text-muted-foreground">{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : "从未登录"}</td>
<td className="p-2"><div className="flex justify-end gap-1"><Button size="sm" variant="ghost" onClick={() => { const username = window.prompt("用户名", user.username); if (username == null) return; const name = window.prompt("姓名", user.name || ""); if (name == null) return; const email = window.prompt("邮箱(可留空)", user.email || ""); if (email == null) return; update.mutate({ id: user.id, username, name, email }); }}>编辑</Button><Button size="sm" variant="ghost" onClick={() => { const password = window.prompt("输入至少 8 位临时密码"); if (password) resetPassword.mutate({ id: user.id, temporaryPassword: password }); }}>重置密码</Button><Button size="sm" variant="ghost" className="text-destructive" onClick={() => { if (window.confirm(`永久删除用户“${user.username}”?此操作不可撤销。`)) remove.mutate({ id: user.id }); }}><Trash2 className="w-4 h-4" /></Button></div></td>
</tr>
))}</tbody>
</table>
{list.isLoading && <p className="text-sm text-muted-foreground p-4">正在加载…</p>}
{list.error && <p className="text-sm text-destructive p-4">{list.error.message}</p>}
</div>
</section>
</div>
);
}