|
@@ -274,35 +274,6 @@ export const UsersPage = () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 加载状态
|
|
|
|
|
- if (isLoading) {
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <div className="flex justify-between items-center">
|
|
|
|
|
- <Skeleton className="h-8 w-48" />
|
|
|
|
|
- <Skeleton className="h-10 w-32" />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <Skeleton className="h-6 w-1/4" />
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent>
|
|
|
|
|
- <div className="space-y-3">
|
|
|
|
|
- {[...Array(5)].map((_, i) => (
|
|
|
|
|
- <div key={i} className="flex gap-4">
|
|
|
|
|
- <Skeleton className="h-10 flex-1" />
|
|
|
|
|
- <Skeleton className="h-10 flex-1" />
|
|
|
|
|
- <Skeleton className="h-10 flex-1" />
|
|
|
|
|
- <Skeleton className="h-10 w-20" />
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
@@ -354,113 +325,139 @@ export const UsersPage = () => {
|
|
|
</TableRow>
|
|
</TableRow>
|
|
|
</TableHeader>
|
|
</TableHeader>
|
|
|
<TableBody>
|
|
<TableBody>
|
|
|
- {users.map((user) => (
|
|
|
|
|
- <TableRow key={user.id}>
|
|
|
|
|
- <TableCell>
|
|
|
|
|
- {user.avatarFile?.fullUrl ? (
|
|
|
|
|
- <img
|
|
|
|
|
- src={user.avatarFile.fullUrl}
|
|
|
|
|
- alt={user.nickname || user.username}
|
|
|
|
|
- className="w-8 h-8 rounded-full object-cover"
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-xs text-gray-500">
|
|
|
|
|
- {user.nickname ? user.nickname.charAt(0).toUpperCase() : user.username.charAt(0).toUpperCase()}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell className="font-medium">{user.username}</TableCell>
|
|
|
|
|
- <TableCell>{user.nickname || '-'}</TableCell>
|
|
|
|
|
- <TableCell>{user.email || '-'}</TableCell>
|
|
|
|
|
- <TableCell>{user.name || '-'}</TableCell>
|
|
|
|
|
- <TableCell>
|
|
|
|
|
- <Badge
|
|
|
|
|
- variant={
|
|
|
|
|
- user.userType === UserType.TEACHER ? 'default' :
|
|
|
|
|
- user.userType === UserType.STUDENT ? 'secondary' : 'outline'
|
|
|
|
|
- }
|
|
|
|
|
- className={
|
|
|
|
|
- user.userType === UserType.TEACHER ? 'bg-blue-100 text-blue-800' :
|
|
|
|
|
- user.userType === UserType.STUDENT ? 'bg-green-100 text-green-800' :
|
|
|
|
|
- 'bg-purple-100 text-purple-800'
|
|
|
|
|
- }
|
|
|
|
|
- >
|
|
|
|
|
- {user.userType === UserType.TEACHER ? '老师' :
|
|
|
|
|
- user.userType === UserType.STUDENT ? '学生' : '学员'}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell>
|
|
|
|
|
- {user.userType === 'trainee' && user.traineeValidFrom && user.traineeValidTo ? (
|
|
|
|
|
- <div className="text-sm">
|
|
|
|
|
- <div>{format(new Date(user.traineeValidFrom), 'yyyy-MM-dd')}</div>
|
|
|
|
|
- <div>至</div>
|
|
|
|
|
- <div>{format(new Date(user.traineeValidTo), 'yyyy-MM-dd')}</div>
|
|
|
|
|
|
|
+ {isLoading ? (
|
|
|
|
|
+ // 加载状态 - 表格骨架屏
|
|
|
|
|
+ [...Array(5)].map((_, index) => (
|
|
|
|
|
+ <TableRow key={index}>
|
|
|
|
|
+ <TableCell><Skeleton className="w-8 h-8 rounded-full" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-4 w-24" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-4 w-20" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-4 w-32" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-4 w-16" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-6 w-12" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-4 w-20" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-6 w-10" /></TableCell>
|
|
|
|
|
+ <TableCell><Skeleton className="h-4 w-24" /></TableCell>
|
|
|
|
|
+ <TableCell className="text-right">
|
|
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
|
|
+ <Skeleton className="h-8 w-8 rounded-md" />
|
|
|
|
|
+ <Skeleton className="h-8 w-8 rounded-md" />
|
|
|
|
|
+ <Skeleton className="h-8 w-8 rounded-md" />
|
|
|
</div>
|
|
</div>
|
|
|
- ) : '-'}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell>
|
|
|
|
|
- <Badge
|
|
|
|
|
- variant={user.isDisabled === 0 ? 'default' : 'destructive'}
|
|
|
|
|
- className={user.isDisabled === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}
|
|
|
|
|
- >
|
|
|
|
|
- {user.isDisabled === 0 ? '启用' : '禁用'}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell>
|
|
|
|
|
- {user.createdAt ? format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN }) : '-'}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell className="text-right">
|
|
|
|
|
- <div className="flex justify-end gap-2">
|
|
|
|
|
- {user.userType === 'trainee' ? (
|
|
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : users.length > 0 ? (
|
|
|
|
|
+ // 正常数据展示
|
|
|
|
|
+ users.map((user) => (
|
|
|
|
|
+ <TableRow key={user.id}>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ {user.avatarFile?.fullUrl ? (
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={user.avatarFile.fullUrl}
|
|
|
|
|
+ alt={user.nickname || user.username}
|
|
|
|
|
+ className="w-8 h-8 rounded-full object-cover"
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-xs text-gray-500">
|
|
|
|
|
+ {user.nickname ? user.nickname.charAt(0).toUpperCase() : user.username.charAt(0).toUpperCase()}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="font-medium">{user.username}</TableCell>
|
|
|
|
|
+ <TableCell>{user.nickname || '-'}</TableCell>
|
|
|
|
|
+ <TableCell>{user.email || '-'}</TableCell>
|
|
|
|
|
+ <TableCell>{user.name || '-'}</TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ <Badge
|
|
|
|
|
+ variant={
|
|
|
|
|
+ user.userType === UserType.TEACHER ? 'default' :
|
|
|
|
|
+ user.userType === UserType.STUDENT ? 'secondary' : 'outline'
|
|
|
|
|
+ }
|
|
|
|
|
+ className={
|
|
|
|
|
+ user.userType === UserType.TEACHER ? 'bg-blue-100 text-blue-800' :
|
|
|
|
|
+ user.userType === UserType.STUDENT ? 'bg-green-100 text-green-800' :
|
|
|
|
|
+ 'bg-purple-100 text-purple-800'
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ {user.userType === UserType.TEACHER ? '老师' :
|
|
|
|
|
+ user.userType === UserType.STUDENT ? '学生' : '学员'}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ {user.userType === 'trainee' && user.traineeValidFrom && user.traineeValidTo ? (
|
|
|
|
|
+ <div className="text-sm">
|
|
|
|
|
+ <div>{format(new Date(user.traineeValidFrom), 'yyyy-MM-dd')}</div>
|
|
|
|
|
+ <div>至</div>
|
|
|
|
|
+ <div>{format(new Date(user.traineeValidTo), 'yyyy-MM-dd')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : '-'}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ <Badge
|
|
|
|
|
+ variant={user.isDisabled === 0 ? 'default' : 'destructive'}
|
|
|
|
|
+ className={user.isDisabled === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}
|
|
|
|
|
+ >
|
|
|
|
|
+ {user.isDisabled === 0 ? '启用' : '禁用'}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ {user.createdAt ? format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN }) : '-'}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="text-right">
|
|
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
|
|
+ {user.userType === 'trainee' ? (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => showRemoveTraineeDialog(user)}
|
|
|
|
|
+ className="text-purple-600 hover:text-purple-800"
|
|
|
|
|
+ title="撤销学员身份"
|
|
|
|
|
+ >
|
|
|
|
|
+ <UserX className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => showSetTraineeDialog(user)}
|
|
|
|
|
+ className="text-purple-600 hover:text-purple-800"
|
|
|
|
|
+ title="设置为学员"
|
|
|
|
|
+ >
|
|
|
|
|
+ <UserCheck className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
<Button
|
|
<Button
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
- onClick={() => showRemoveTraineeDialog(user)}
|
|
|
|
|
- className="text-purple-600 hover:text-purple-800"
|
|
|
|
|
- title="撤销学员身份"
|
|
|
|
|
|
|
+ onClick={() => showEditModal(user)}
|
|
|
>
|
|
>
|
|
|
- <UserX className="h-4 w-4" />
|
|
|
|
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
</Button>
|
|
</Button>
|
|
|
- ) : (
|
|
|
|
|
<Button
|
|
<Button
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
- onClick={() => showSetTraineeDialog(user)}
|
|
|
|
|
- className="text-purple-600 hover:text-purple-800"
|
|
|
|
|
- title="设置为学员"
|
|
|
|
|
|
|
+ onClick={() => showDeleteDialog(user.id)}
|
|
|
|
|
+ className="text-destructive hover:text-destructive"
|
|
|
>
|
|
>
|
|
|
- <UserCheck className="h-4 w-4" />
|
|
|
|
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
</Button>
|
|
</Button>
|
|
|
- )}
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => showEditModal(user)}
|
|
|
|
|
- >
|
|
|
|
|
- <Edit className="h-4 w-4" />
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => showDeleteDialog(user.id)}
|
|
|
|
|
- className="text-destructive hover:text-destructive"
|
|
|
|
|
- >
|
|
|
|
|
- <Trash2 className="h-4 w-4" />
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ // 空数据状态
|
|
|
|
|
+ <TableRow>
|
|
|
|
|
+ <TableCell colSpan={10} className="h-24 text-center">
|
|
|
|
|
+ <p className="text-muted-foreground">暂无数据</p>
|
|
|
</TableCell>
|
|
</TableCell>
|
|
|
</TableRow>
|
|
</TableRow>
|
|
|
- ))}
|
|
|
|
|
|
|
+ )}
|
|
|
</TableBody>
|
|
</TableBody>
|
|
|
</Table>
|
|
</Table>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {users.length === 0 && (
|
|
|
|
|
- <div className="text-center py-8">
|
|
|
|
|
- <p className="text-muted-foreground">暂无数据</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
{/* 分页 */}
|
|
{/* 分页 */}
|
|
|
<DataTablePagination
|
|
<DataTablePagination
|
|
|
currentPage={searchParams.page}
|
|
currentPage={searchParams.page}
|