Files
GovAI/apps/web/src/app/(admin)/users/page.tsx
T
2026-06-15 23:48:37 +08:00

161 lines
5.4 KiB
TypeScript

"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import api from "@/lib/api";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { toast } from "sonner";
interface User {
id: string;
name: string;
email: string;
avatar_url?: string;
role: string;
status: string;
employee_id?: string;
last_login_at?: string;
login_count: number;
created_at: string;
}
const roleLabels: Record<string, string> = {
super_admin: "平台管理员",
admin: "机构管理员",
creator: "创作者",
user: "普通用户",
};
const roleColors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
super_admin: "destructive",
admin: "default",
creator: "secondary",
user: "outline",
};
export default function UsersPage() {
const [search, setSearch] = useState("");
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ["adminUsers", search],
queryFn: () => api.get<{ items: User[] }>(`/api/v1/admin/users?q=${search}`),
});
const updateRole = useMutation({
mutationFn: ({ id, role }: { id: string; role: string }) =>
api.put(`/api/v1/admin/users/${id}/role`, { role }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["adminUsers"] });
toast.success("角色更新成功");
},
onError: (err: Error) => toast.error(err.message),
});
const updateStatus = useMutation({
mutationFn: ({ id, status }: { id: string; status: string }) =>
api.put(`/api/v1/admin/users/${id}/status`, { status }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["adminUsers"] });
toast.success("状态更新成功");
},
onError: (err: Error) => toast.error(err.message),
});
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold"></h1>
<Input
placeholder="搜索姓名或邮箱..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-64"
/>
</div>
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="text-left p-3"></th>
<th className="text-left p-3"></th>
<th className="text-left p-3"></th>
<th className="text-left p-3"></th>
<th className="text-left p-3"></th>
</tr>
</thead>
<tbody>
{data?.items?.map((user) => (
<tr key={user.id} className="border-t">
<td className="p-3">
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatar_url} />
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{user.name}</div>
<div className="text-xs text-muted-foreground">{user.email}</div>
</div>
</div>
</td>
<td className="p-3">
<Badge variant={roleColors[user.role]}>
{roleLabels[user.role]}
</Badge>
</td>
<td className="p-3">
<Badge variant={user.status === "active" ? "default" : "destructive"}>
{user.status === "active" ? "正常" : "禁用"}
</Badge>
</td>
<td className="p-3 text-muted-foreground">{user.login_count}</td>
<td className="p-3">
<div className="flex items-center gap-2">
<Select
defaultValue={user.role}
onValueChange={(role) => role && updateRole.mutate({ id: user.id, role })}
>
<SelectTrigger className="w-28 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user"></SelectItem>
<SelectItem value="creator"></SelectItem>
<SelectItem value="admin"></SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() =>
updateStatus.mutate({
id: user.id,
status: user.status === "active" ? "disabled" : "active",
})
}
>
{user.status === "active" ? "禁用" : "启用"}
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}