Files
AOUN/src/pages/admin/users/index.tsx
2025-11-28 22:44:54 +08:00

314 lines
14 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, useEffect } from 'react';
import AdminLayout from '@/components/admin/AdminLayout';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Search, Edit, Trash2, MoreHorizontal, Loader2 } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface User {
_id: string;
用户名: string;
邮箱: string;
角色: string;
是否被封禁: boolean;
头像: string;
createdAt: string;
}
export default function UserManagement() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalUsers, setTotalUsers] = useState(0);
// 编辑状态
const [editingUser, setEditingUser] = useState<User | null>(null);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editForm, setEditForm] = useState({ role: '', isBanned: false });
// 搜索防抖
useEffect(() => {
const timer = setTimeout(() => {
fetchUsers();
}, 500);
return () => clearTimeout(timer);
}, [search, page]);
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch(`/api/admin/users?page=${page}&limit=10&search=${search}`);
const data = await res.json();
if (res.ok) {
setUsers(data.users);
setTotalPages(data.totalPages);
setTotalUsers(data.total);
}
} catch (error) {
console.error('Failed to fetch users', error);
} finally {
setLoading(false);
}
};
const handleEditClick = (user: User) => {
setEditingUser(user);
setEditForm({ role: user.角色, isBanned: user.是否被封禁 });
setIsEditDialogOpen(true);
};
const handleUpdateUser = async () => {
if (!editingUser) return;
try {
const res = await fetch(`/api/admin/users/${editingUser._id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editForm),
});
if (res.ok) {
setIsEditDialogOpen(false);
fetchUsers(); // 刷新列表
} else {
alert('更新失败');
}
} catch (error) {
console.error('Update failed', error);
}
};
const handleDeleteUser = async (id: string) => {
if (!confirm('确定要删除该用户吗?此操作不可恢复。')) return;
try {
const res = await fetch(`/api/admin/users/${id}`, {
method: 'DELETE',
});
if (res.ok) {
fetchUsers();
} else {
const data = await res.json();
alert(data.message || '删除失败');
}
} catch (error) {
console.error('Delete failed', error);
}
};
return (
<AdminLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground">
</p>
</div>
{/* <Button>添加用户</Button> */}
</div>
<div className="flex items-center justify-between gap-4">
<div className="relative w-full max-w-sm">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索用户名或邮箱..."
className="pl-8"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="rounded-md border bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[80px]"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
<Loader2 className="mx-auto h-6 w-6 animate-spin" />
</TableCell>
</TableRow>
) : users.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
</TableCell>
</TableRow>
) : (
users.map((user) => (
<TableRow key={user._id}>
<TableCell>
<Avatar>
<AvatarImage src={user.} alt={user.} />
<AvatarFallback>{user..slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
</TableCell>
<TableCell className="font-medium">{user.}</TableCell>
<TableCell>{user.}</TableCell>
<TableCell>
<Badge variant={user. === 'admin' ? 'default' : 'secondary'}>
{user.}
</Badge>
</TableCell>
<TableCell>
{user. ? (
<Badge variant="destructive"></Badge>
) : (
<Badge variant="outline" className="text-green-600 border-green-200 bg-green-50"></Badge>
)}
</TableCell>
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only"></span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem onClick={() => handleEditClick(user)}>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => handleDeleteUser(user._id)}
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{totalUsers} {page} / {totalPages}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1 || loading}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages || loading}
>
</Button>
</div>
{/* 编辑弹窗 */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{editingUser?.}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="role" className="text-right text-sm font-medium">
</label>
<Select
value={editForm.role}
onValueChange={(val) => setEditForm({ ...editForm, role: val })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="选择角色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">User ()</SelectItem>
<SelectItem value="editor">Editor ()</SelectItem>
<SelectItem value="admin">Admin ()</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="status" className="text-right text-sm font-medium">
</label>
<Select
value={editForm.isBanned ? 'banned' : 'active'}
onValueChange={(val) => setEditForm({ ...editForm, isBanned: val === 'banned' })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="选择状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="banned"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={handleUpdateUser}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</AdminLayout>
);
}