381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { DashboardShell } from "@/components/dashboard-shell"
|
|
import { DashboardHeader } from "@/components/dashboard-header"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Search, Edit, Loader2 } from "lucide-react"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { UserDetailDialog } from "@/components/users/user-detail-dialog"
|
|
import { UserFormDialog } from "@/components/users/user-form-dialog"
|
|
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
|
import { useToast } from "@/components/ui/use-toast"
|
|
import { usersApi, rolesApi, handleApiError } from "@/lib/api"
|
|
|
|
export default function UsersPage() {
|
|
const [users, setUsers] = useState([])
|
|
const [roles, setRoles] = useState([])
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
const [totalPages, setTotalPages] = useState(1)
|
|
const [totalUsers, setTotalUsers] = useState(0)
|
|
const [editingUser, setEditingUser] = useState(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const { toast } = useToast()
|
|
|
|
const itemsPerPage = 5
|
|
|
|
// 获取用户列表
|
|
const fetchUsers = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const response = await usersApi.getUsers({
|
|
page: currentPage,
|
|
pageSize: itemsPerPage,
|
|
search: searchQuery,
|
|
})
|
|
|
|
setUsers(response.items)
|
|
setTotalPages(response.totalPages)
|
|
setTotalUsers(response.total)
|
|
} catch (error) {
|
|
toast({
|
|
title: "获取用户列表失败",
|
|
description: handleApiError(error),
|
|
variant: "destructive",
|
|
})
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
// 获取角色列表
|
|
const fetchRoles = async () => {
|
|
try {
|
|
const response = await rolesApi.getRoles()
|
|
setRoles(response.items)
|
|
} catch (error) {
|
|
toast({
|
|
title: "获取角色列表失败",
|
|
description: handleApiError(error),
|
|
variant: "destructive",
|
|
})
|
|
}
|
|
}
|
|
|
|
// 初始加载
|
|
useEffect(() => {
|
|
fetchRoles()
|
|
}, [])
|
|
|
|
// 当页码、搜索条件变化时重新获取数据
|
|
useEffect(() => {
|
|
fetchUsers()
|
|
}, [currentPage, searchQuery])
|
|
|
|
// 添加用户
|
|
const handleAddUser = async (data) => {
|
|
try {
|
|
await usersApi.createUser({
|
|
name: data.name,
|
|
email: data.email,
|
|
role: data.role,
|
|
status: data.status,
|
|
phone: data.phone,
|
|
address: data.address,
|
|
})
|
|
|
|
toast({
|
|
title: "用户创建成功",
|
|
description: `用户 "${data.name}" 已成功创建`,
|
|
variant: "default",
|
|
})
|
|
|
|
fetchUsers() // 刷新用户列表
|
|
} catch (error) {
|
|
toast({
|
|
title: "创建用户失败",
|
|
description: handleApiError(error),
|
|
variant: "destructive",
|
|
})
|
|
throw error // 重新抛出错误,让表单组件处理
|
|
}
|
|
}
|
|
|
|
// 编辑用户
|
|
const handleEditUser = async (data) => {
|
|
if (!editingUser) return
|
|
|
|
try {
|
|
await usersApi.updateUser(editingUser.id, {
|
|
name: data.name,
|
|
email: data.email,
|
|
role: data.role,
|
|
status: data.status,
|
|
phone: data.phone,
|
|
address: data.address,
|
|
})
|
|
|
|
toast({
|
|
title: "用户更新成功",
|
|
description: `用户 "${data.name}" 已成功更新`,
|
|
variant: "default",
|
|
})
|
|
|
|
setEditingUser(null)
|
|
fetchUsers() // 刷新用户列表
|
|
} catch (error) {
|
|
toast({
|
|
title: "更新用户失败",
|
|
description: handleApiError(error),
|
|
variant: "destructive",
|
|
})
|
|
throw error // 重新抛出错误,让表单组件处理
|
|
}
|
|
}
|
|
|
|
// 删除用户
|
|
const handleDeleteUser = async (userId) => {
|
|
try {
|
|
await usersApi.deleteUser(userId)
|
|
|
|
toast({
|
|
title: "用户删除成功",
|
|
description: "用户已成功删除",
|
|
variant: "default",
|
|
})
|
|
|
|
fetchUsers() // 刷新用户列表
|
|
} catch (error) {
|
|
toast({
|
|
title: "删除用户失败",
|
|
description: handleApiError(error),
|
|
variant: "destructive",
|
|
})
|
|
}
|
|
}
|
|
|
|
// 开始编辑用户
|
|
const startEditUser = (user) => {
|
|
setEditingUser(user)
|
|
}
|
|
|
|
// 获取头像颜色
|
|
const getAvatarColor = (name) => {
|
|
const colors = [
|
|
"from-pink-500 to-purple-500",
|
|
"from-blue-500 to-teal-500",
|
|
"from-red-500 to-orange-500",
|
|
"from-green-500 to-emerald-500",
|
|
"from-purple-500 to-indigo-500",
|
|
]
|
|
|
|
// Simple hash function to pick a color based on name
|
|
const hash = name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
|
|
return colors[hash % colors.length]
|
|
}
|
|
|
|
// 获取用户名首字母
|
|
const getInitials = (name) => {
|
|
return name
|
|
.split(" ")
|
|
.map((part) => part[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.substring(0, 2)
|
|
}
|
|
|
|
return (
|
|
<DashboardShell>
|
|
<DashboardHeader heading="用户管理" text="管理系统用户和权限">
|
|
<UserFormDialog
|
|
mode="add"
|
|
onSubmit={handleAddUser}
|
|
roles={roles.map((role) => ({ id: role.id, name: role.name }))}
|
|
/>
|
|
</DashboardHeader>
|
|
|
|
<div className="flex items-center justify-between space-y-2 mb-6">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="search"
|
|
placeholder="搜索用户..."
|
|
className="w-[300px] pl-8 border-none bg-white shadow-md focus-visible:ring-pink-500"
|
|
value={searchQuery}
|
|
onChange={(e) => {
|
|
setSearchQuery(e.target.value)
|
|
setCurrentPage(1) // 重置到第一页
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-purple-50">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl font-bold flex items-center">
|
|
<span className="bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">用户列表</span>
|
|
<div className="ml-2 h-1 w-10 bg-gradient-to-r from-purple-600 to-pink-600 rounded-full"></div>
|
|
</CardTitle>
|
|
<CardDescription>管理系统用户账户</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader className="bg-gray-50">
|
|
<TableRow>
|
|
<TableHead className="w-[50px]"></TableHead>
|
|
<TableHead>用户名</TableHead>
|
|
<TableHead>邮箱</TableHead>
|
|
<TableHead>角色</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead>注册日期</TableHead>
|
|
<TableHead>最后登录</TableHead>
|
|
<TableHead className="text-right">操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="h-24 text-center">
|
|
<div className="flex justify-center items-center">
|
|
<Loader2 className="h-6 w-6 animate-spin text-purple-500 mr-2" />
|
|
<span>加载中...</span>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : users.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="h-24 text-center text-muted-foreground">
|
|
没有找到匹配的用户
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
users.map((user) => (
|
|
<TableRow key={user.id} className="hover:bg-gray-50 transition-colors">
|
|
<TableCell>
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={user.avatar} alt={user.name} />
|
|
<AvatarFallback className={`bg-gradient-to-br ${getAvatarColor(user.name)} text-white`}>
|
|
{getInitials(user.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</TableCell>
|
|
<TableCell className="font-medium">{user.name}</TableCell>
|
|
<TableCell>{user.email}</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
className={
|
|
user.role === "超级管理员"
|
|
? "bg-purple-500"
|
|
: user.role === "内容管理员"
|
|
? "bg-blue-500"
|
|
: user.role === "AI模型管理员"
|
|
? "bg-teal-500"
|
|
: user.role === "卡牌管理员"
|
|
? "bg-pink-500"
|
|
: "bg-orange-500"
|
|
}
|
|
>
|
|
{user.role}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
className={
|
|
user.status === "活跃"
|
|
? "bg-green-500"
|
|
: user.status === "未激活"
|
|
? "bg-gray-500"
|
|
: "bg-red-500"
|
|
}
|
|
>
|
|
{user.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>{user.registeredAt}</TableCell>
|
|
<TableCell>{user.lastLogin || "-"}</TableCell>
|
|
<TableCell className="text-right">
|
|
<UserDetailDialog user={user} onEdit={() => startEditUser(user)} />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="hover:bg-pink-50 hover:text-pink-600"
|
|
onClick={() => startEditUser(user)}
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
|
|
{user.role !== "超级管理员" && (
|
|
<DeleteConfirmationDialog
|
|
title="删除用户"
|
|
description="此操作将永久删除该用户,且无法恢复。"
|
|
itemName={user.name}
|
|
onDelete={() => handleDeleteUser(user.id)}
|
|
/>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-between">
|
|
<div className="text-sm text-muted-foreground">
|
|
显示 {users.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}-
|
|
{Math.min(currentPage * itemsPerPage, totalUsers)} 共 {totalUsers} 个用户
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
|
|
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
|
disabled={currentPage === 1 || isLoading}
|
|
>
|
|
上一页
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
|
|
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
|
disabled={currentPage === totalPages || totalPages === 0 || isLoading}
|
|
>
|
|
下一页
|
|
</Button>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
|
|
{/* 编辑用户对话框 */}
|
|
{editingUser && (
|
|
<UserFormDialog
|
|
mode="edit"
|
|
open={!!editingUser}
|
|
onOpenChange={(open) => {
|
|
if (!open) setEditingUser(null)
|
|
}}
|
|
onSubmit={handleEditUser}
|
|
defaultValues={{
|
|
name: editingUser.name,
|
|
email: editingUser.email,
|
|
role: editingUser.role,
|
|
status: editingUser.status,
|
|
phone: editingUser.phone,
|
|
address: editingUser.address,
|
|
}}
|
|
roles={roles.map((role) => ({ id: role.id, name: role.name }))}
|
|
/>
|
|
)}
|
|
</DashboardShell>
|
|
)
|
|
}
|