161 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|