Răsfoiți Sursa

✨ feat(admin): 优化用户管理页面加载体验

- 将顶部骨架屏整合到表格中,实现统一的加载状态展示
- 为表格行添加骨架屏,提升数据加载过程中的视觉体验
- 优化空数据展示,使用表格内居中显示"暂无数据"提示
- 调整表格布局,确保骨架屏与实际数据布局一致
yourname 6 luni în urmă
părinte
comite
6523d0fee9
1 a modificat fișierele cu 116 adăugiri și 119 ștergeri
  1. 116 119
      src/client/admin/pages/Users.tsx

+ 116 - 119
src/client/admin/pages/Users.tsx

@@ -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 (
     <div className="space-y-4">
@@ -354,113 +325,139 @@ export const UsersPage = () => {
                 </TableRow>
               </TableHeader>
               <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>
-                      ) : '-'}
-                    </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
                             variant="ghost"
                             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
                             variant="ghost"
                             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
-                          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>
                   </TableRow>
-                ))}
+                )}
               </TableBody>
             </Table>
           </div>
 
-          {users.length === 0 && (
-            <div className="text-center py-8">
-              <p className="text-muted-foreground">暂无数据</p>
-            </div>
-          )}
-
           {/* 分页 */}
           <DataTablePagination
             currentPage={searchParams.page}