# P10-NS4-06：表格组件的统一规范

## 简要结论
- 状态：⚠️ 部分解决
- tenant-admin 各页面表格在分页大小、loading 状态、筛选器位置上已形成事实一致的模式，但缺少显式的统一配置抽象层；排序交互完全缺失；空数据展示未统一；部分表格分页大小不一致（20 vs 10）。

## 详细审查

### 前端代码

#### 1. 分页大小

| 页面/组件 | 默认 pageSize | showSizeChanger | showTotal | 后端分页 |
|-----------|:---:|:---:|:---:|:---:|
| UserApproval | 20 | ✅ | ✅ `共 N 条` | ✅ page/page_size |
| UserManagement | 20 | ✅ | ✅ `共 N 条` | ✅ page/page_size |
| ExcelUpload — 上传记录 | 20 | ✅ | ✅ `共 N 条` | ✅ page/page_size |
| ExcelUpload — 校验详情 | **10** | ❌ | ❌ | 前端分页 |
| RetentionClues — 客户搜索 | `false` | — | — | 无分页（LIMIT 50） |
| RetentionClues — 线索列表 | **10** | ❌ | ❌ | 无分页（全量返回） |
| DiffTable — 冲突表 | `false` | — | — | 前端数据 |

结论：主列表页统一为 20 条/页 + showSizeChanger + showTotal，但子表格（校验详情、线索列表）使用 10 条/页且缺少 showSizeChanger 和 showTotal，**不一致**。

#### 2. 排序交互

所有 5 个表格组件均未配置 `sorter` 属性，前端不支持列排序。后端 SQL 统一使用 `ORDER BY created_at DESC`（固定倒序），无动态排序参数。

结论：**完全缺失**。用户无法按任意列排序。

#### 3. 筛选器位置

| 页面 | 筛选器位置 | 筛选方式 |
|------|-----------|---------|
| UserApproval | 表格上方独立区域 | Select（状态筛选） |
| UserManagement | 表格上方独立区域 | Select（角色）+ Input.Search（关键词） |
| ExcelUpload — 上传记录 | 无筛选 | — |
| RetentionClues | Card title 区域 + Card extra 区域 | Input.Search + SiteSelector（上方）；Select×2（Card extra 右侧） |

结论：筛选器统一放在表格上方（非表格内 column filter），**位置一致**。但 RetentionClues 的线索筛选器放在 Card extra 右侧，与其他页面的左对齐布局略有差异。

#### 4. loading 状态

所有带后端请求的表格均通过 `loading={loading}` 传入 Ant Design Table 的 loading 属性，**统一且正确**。

#### 5. 空数据展示

| 页面 | 空数据处理 |
|------|-----------|
| UserApproval | Ant Design Table 默认空状态（无自定义） |
| UserManagement | Ant Design Table 默认空状态（无自定义） |
| ExcelUpload | Ant Design Table 默认空状态（无自定义） |
| RetentionClues — 客户搜索 | `<Empty description="未找到匹配客户" />` ✅ 自定义 |
| RetentionClues — 线索列表 | Ant Design Table 默认空状态（无自定义） |
| DiffTable | Ant Design Table 默认空状态（无自定义） |

结论：仅 RetentionClues 客户搜索有自定义空状态文案，其余均依赖 Ant Design 默认的英文 "No Data"。**未统一**，且未做中文化。

对比 admin-web：`TaskManager` 页面使用了 `locale={{ emptyText: <Empty description="队列为空" /> }}` 自定义空状态，tenant-admin 未跟进。

#### 6. 统一配置抽象

- tenant-admin 中**不存在**统一的表格配置文件、常量定义或封装组件
- 每个页面独立定义 `pageSize`、`pagination` 配置、`handleTableChange` 等
- 分页响应结构 `{ items, total, page, pageSize }` 在前后端已统一，但属于隐式约定

### 后端代码

#### 分页参数统一性

| 路由 | 参数名 | 默认值 | 范围约束 |
|------|--------|--------|---------|
| `GET /applications` | `page` + `page_size` | 1 / 20 | ge=1 / ge=1,le=100 |
| `GET /users` | `page` + `page_size` | 1 / 20 | ge=1 / ge=1,le=100 |
| `GET /excel/logs` | `page` + `page_size` | 1 / 20 | ge=1 / ge=1,le=100 |
| `GET /customers/search` | 无分页 | — | LIMIT 50 硬编码 |
| `GET /customers/{id}/clues` | 无分页 | — | 全量返回 |

结论：三个主列表接口的分页参数**完全统一**（`page`/`page_size`，默认 20，上限 100）。客户搜索和线索列表不分页，属于业务设计选择（数据量可控），但线索列表在数据量增长后可能需要补充分页。

响应格式统一为 `{ items: T[], total: int, page: int, pageSize: int }`，**一致**。

### 差距分析

与 P10 标杆文件要求的差距：

| 规范项 | P10 要求（推断） | 当前状态 | 差距 |
|--------|-----------------|---------|------|
| 默认分页大小 | 统一值（如 20） | 主表 20，子表 10 | 🟡 子表不一致 |
| showSizeChanger | 所有分页表格启用 | 仅主表启用 | 🟡 子表缺失 |
| showTotal | 所有分页表格显示 | 仅主表显示 | 🟡 子表缺失 |
| 排序交互 | 至少关键列支持排序 | 完全缺失 | 🔴 缺失 |
| 筛选器位置 | 统一在表格上方 | 基本一致 | ✅ |
| loading 状态 | 统一 loading 属性 | 全部已配置 | ✅ |
| 空数据展示 | 统一中文空状态 | 仅 1 处自定义，其余默认 | 🟡 未统一 |
| 统一配置抽象 | 提取公共 Table 配置 | 不存在 | 🔴 缺失 |

### 建议

1. **提取统一表格配置常量**（优先级：高）

   在 `apps/tenant-admin/src/constants/table.ts` 中定义：
   ```ts
   export const DEFAULT_PAGE_SIZE = 20;
   export const PAGE_SIZE_OPTIONS = ['10', '20', '50'];
   export const TABLE_LOCALE = {
     emptyText: '暂无数据',
   };
   export const defaultPagination = (total: number, page: number, pageSize: number) => ({
     current: page,
     pageSize,
     total,
     showSizeChanger: true,
     showTotal: (t: number) => `共 ${t} 条`,
     pageSizeOptions: PAGE_SIZE_OPTIONS,
   });
   ```

2. **统一空数据展示**（优先级：高）

   通过 Ant Design ConfigProvider 全局设置中文 locale 和空状态：
   ```tsx
   import zhCN from 'antd/locale/zh_CN';
   <ConfigProvider locale={zhCN} renderEmpty={() => <Empty description="暂无数据" />}>
   ```

3. **子表格分页对齐**（优先级：中）

   ExcelUpload 校验详情和 RetentionClues 线索列表的 pageSize 改为 20，并补充 showSizeChanger 和 showTotal。

4. **排序交互**（优先级：低）

   对时间列（申请时间、上传时间、记录时间）添加前端 `sorter` 支持。如数据量增长需后端排序，可在 API 中增加 `sort_by` + `sort_order` 参数。当前数据量下前端排序即可。

5. **线索列表补充分页**（优先级：低）

   `GET /customers/{member_id}/clues` 当前全量返回，建议在数据量可能超过 50 条时补充后端分页。
