2
0
Просмотр исходного кода

📝 docs(crud): 完善骨架屏实现策略和加载状态管理

- 重构骨架屏章节,增加"骨架屏实现策略"小节
- 新增四种骨架屏实现方式:页面级加载、表格级加载、简化骨架屏和区分加载状态逻辑
- 详细说明React Query的isLoading和isFetching状态区分用法
- 增加"最佳实践总结",提炼骨架屏使用原则
- 提供Users.tsx完整示例实现,展示正确的加载状态处理
- 新增"React Query 状态管理详解",详细说明数据获取状态组合使用方式
yourname 6 месяцев назад
Родитель
Сommit
f0d4a85149
1 измененных файлов с 213 добавлено и 8 удалено
  1. 213 8
      .claude/agents/generic-crud-page.md

+ 213 - 8
.claude/agents/generic-crud-page.md

@@ -596,9 +596,12 @@ const handleCreateSubmit = async (data: CreateRequest) => {
 import { Skeleton } from '@/client/components/ui/skeleton';
 ```
 
-###### 完整骨架屏实现
+###### 骨架屏实现策略
+
+**1. 页面级加载(首次加载)**
+适用于首次进入页面时的完整加载,替换整个页面内容:
 ```tsx
-if (isLoading) {
+if (isLoading && !data) {
   return (
     <div className="space-y-4">
       {/* 标题区域骨架 */}
@@ -652,9 +655,53 @@ if (isLoading) {
 }
 ```
 
-###### 简化骨架屏(推荐)
+**2. 表格级加载(搜索/翻页)**
+适用于搜索、翻页等操作时的局部加载,保持页面结构不变:
 ```tsx
-if (isLoading) {
+<TableBody>
+  {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>
+      </TableRow>
+    ))
+  ) : data.length > 0 ? (
+    // 正常数据展示
+    data.map((item) => (
+      <TableRow key={item.id}>
+        {/* 数据行内容 */}
+      </TableRow>
+    ))
+  ) : (
+    // 空数据状态
+    <TableRow>
+      <TableCell colSpan={10} className="text-center py-8">
+        <p className="text-muted-foreground">暂无数据</p>
+      </TableCell>
+    </TableRow>
+  )}
+</TableBody>
+```
+
+**3. 简化骨架屏(推荐用于简单列表)**
+```tsx
+if (isLoading && !data) {
   return (
     <div className="space-y-4">
       <div className="flex justify-between items-center">
@@ -681,15 +728,173 @@ if (isLoading) {
 }
 ```
 
+**4. 区分加载状态的逻辑**
+```typescript
+// 使用React Query的isLoading和isFetching区分加载类型
+const { data, isLoading, isFetching } = useQuery({
+  queryKey: ['entities', searchParams],
+  queryFn: async () => {
+    const res = await entityClient.$get({
+      query: searchParams
+    });
+    if (res.status !== 200) throw new Error('获取列表失败');
+    return await res.json();
+  }
+});
+
+// 首次加载:isLoading为true,data为undefined
+// 后台刷新:isFetching为true,data有值
+
+// 可以根据不同状态显示不同的骨架屏
+const showFullSkeleton = isLoading && !data; // 首次加载
+const showTableSkeleton = isFetching && data; // 搜索/翻页加载
+
 ##### 空数据状态
 ```tsx
-{users.length === 0 && !isLoading && (
-  <div className="text-center py-8">
-    <p className="text-muted-foreground">暂无数据</p>
-  </div>
+{data?.length === 0 && !isLoading && (
+  <TableRow>
+    <TableCell colSpan={10} className="text-center py-8">
+      <p className="text-muted-foreground">暂无数据</p>
+    </TableCell>
+  </TableRow>
 )}
 ```
 
+##### 最佳实践总结
+
+1. **首次加载**: 使用页面级骨架屏,替换整个页面内容
+2. **搜索/翻页**: 使用表格级骨架屏,保持页面结构不变
+3. **区分状态**: 使用 `isLoading` 和 `isFetching` 区分不同加载类型
+4. **空数据**: 在表格内部显示空数据状态,保持布局一致性
+5. **用户体验**: 避免全屏刷新,提供平滑的加载过渡
+
+##### Users.tsx 示例实现
+```tsx
+// 正确的方式:在TableBody内部处理加载状态
+<TableBody>
+  {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>
+      </TableRow>
+    ))
+  ) : users.length > 0 ? (
+    // 正常数据展示
+    users.map((user) => (
+      <TableRow key={user.id}>
+        {/* 数据行内容 */}
+      </TableRow>
+    ))
+  ) : (
+    // 空数据状态
+    <TableRow>
+      <TableCell colSpan={10} className="text-center py-8">
+        <p className="text-muted-foreground">暂无数据</p>
+      </TableCell>
+    </TableRow>
+  )}
+</TableBody>
+
+// 错误的方式:避免在组件顶层返回骨架屏
+// if (isLoading) {
+//   return <div>全屏骨架屏...</div>; // 这会导致搜索/翻页时全屏刷新
+// }
+```
+
+##### React Query 状态管理详解
+
+```typescript
+// 使用React Query的完整状态管理
+const { 
+  data,           // 响应数据
+  isLoading,      // 首次加载(无缓存数据)
+  isFetching,     // 任何正在进行的请求
+  isError,        // 请求失败
+  error,          // 错误对象
+  refetch         // 手动重新获取数据
+} = useQuery({
+  queryKey: ['users', searchParams], // 查询键,包含所有依赖参数
+  queryFn: async () => {
+    const res = await userClient.$get({
+      query: {
+        page: searchParams.page,
+        pageSize: searchParams.pageSize,
+        keyword: searchParams.keyword,
+        // 其他查询参数...
+      }
+    });
+    if (res.status !== 200) throw new Error('获取用户列表失败');
+    return await res.json();
+  },
+  keepPreviousData: true, // 保持旧数据直到新数据到达
+  staleTime: 5 * 60 * 1000, // 数据过期时间(5分钟)
+});
+
+// 状态组合示例
+const showFullLoading = isLoading && !data;    // 首次加载,显示完整页面骨架
+const showTableLoading = isFetching && data;   // 后台刷新,显示表格骨架
+const showError = isError;                     // 显示错误状态
+const showData = !isLoading && data;           // 显示正常数据
+
+// 在组件中使用
+return (
+  <div>
+    {/* 页面标题和操作按钮区域 - 始终显示 */}
+    <div className="flex justify-between items-center">
+      <h1 className="text-2xl font-bold">用户管理</h1>
+      <Button onClick={() => setIsCreateModalOpen(true)}>
+        <Plus className="mr-2 h-4 w-4" />
+        创建用户
+      </Button>
+    </div>
+
+    {/* 搜索区域 - 始终显示 */}
+    <Card className="mt-4">
+      <CardContent className="pt-6">
+        <SearchForm onSearch={handleSearch} />
+      </CardContent>
+    </Card>
+
+    {/* 数据表格区域 */}
+    {showFullLoading && (
+      // 首次加载:完整页面骨架
+      <FullPageSkeleton />
+    )}
+    
+    {showTableLoading && (
+      // 后台刷新:表格骨架(保持页面结构)
+      <TableSkeleton />
+    )}
+    
+    {showError && (
+      // 错误状态
+      <ErrorState error={error} onRetry={refetch} />
+    )}
+    
+    {showData && (
+      // 正常数据展示
+      <DataTable data={data} />
+    )}
+  </div>
+);
+```
+
 ### 3. 功能实现
 
 #### 数据表格