refactor: 统一管理页面布局 — FilterToolbar + usePageSearchParams + parseListParams
This commit is contained in:
167
docs/development/crud.md
Normal file
167
docs/development/crud.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# CRUD 管理页面模式
|
||||
|
||||
管理类页面(项目管理、模型管理、供应商管理)遵循统一的 CRUD 模式。本文档描述该模式的后端和前端约定。
|
||||
|
||||
## 背景
|
||||
|
||||
三个管理页面原本各自实现了工具条、筛选、分页、搜索等逻辑,代码重复且交互不一致。统一后所有管理页面使用相同的基础设施。
|
||||
|
||||
## 页面结构
|
||||
|
||||
每个管理页面的组件结构:
|
||||
|
||||
```text
|
||||
<Space className="app-page-flex" orientation="vertical" size="large">
|
||||
<FilterToolbar /> ← 筛选 + 搜索 + 操作按钮
|
||||
<EntityTable /> ← 数据表格(分页 + 排序 + 行操作)
|
||||
<EntityFormModal /> ← 创建/编辑弹窗
|
||||
</Space>
|
||||
```
|
||||
|
||||
## 前端基础设施
|
||||
|
||||
### FilterToolbar
|
||||
|
||||
`src/web/shared/components/FilterToolbar.tsx` — 统一筛选工具条。
|
||||
|
||||
```tsx
|
||||
interface FilterConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
value?: string; // 受控值
|
||||
onChange: (value: string | undefined) => void;
|
||||
}
|
||||
|
||||
interface SearchConfig {
|
||||
placeholder: string;
|
||||
keyword?: string; // 受控关键词(来自 URL)
|
||||
onSearch: (value: string) => void; // 点击搜索按钮
|
||||
onReset: () => void; // 点击重置按钮
|
||||
}
|
||||
|
||||
interface FilterToolbarProps {
|
||||
filters?: FilterConfig[]; // 左侧筛选下拉框
|
||||
search?: SearchConfig; // 搜索输入框 + 重置按钮
|
||||
actions?: ReactNode; // 右侧操作区(如"新建"按钮)
|
||||
}
|
||||
```
|
||||
|
||||
**布局**:左侧 = 筛选 Select + 搜索框(SearchOutlined 按钮) + 重置按钮(UndoOutlined),右侧 = actions 插槽。
|
||||
|
||||
**交互约定**:
|
||||
|
||||
- 筛选下拉框:选中即过滤,调用 `onChange`(实时过滤)
|
||||
- 搜索框:输入文本,点击搜索按钮或 Enter 触发 `onSearch`(点击搜索)
|
||||
- 重置按钮:调用 `onReset` 清除全部筛选条件
|
||||
|
||||
### usePageSearchParams
|
||||
|
||||
`src/web/shared/hooks/usePageSearchParams.ts` — 将页面筛选/分页/排序状态同步到 URL 查询参数。
|
||||
|
||||
```tsx
|
||||
const { params, setParams, resetAll } = usePageSearchParams({
|
||||
defaults: { page: "1", pageSize: "20" },
|
||||
});
|
||||
```
|
||||
|
||||
- `params`:`Record<string, string>`,读取当前 URL 参数(含默认值)
|
||||
- `setParams(patch)`:批量更新多个参数(推荐),内部使用函数式更新器 + `replace: true`
|
||||
- `setParam(key, value)`:更新单个参数(**注意**:连续多次 `setParam` 会导致闭包快照覆盖,多参数更新必须使用 `setParams`)
|
||||
- `resetAll()`:清空所有 URL 参数
|
||||
- 默认值(`defaults`)不出现在 URL 中
|
||||
|
||||
**核心约束**:react-router 的 `setSearchParams` 函数式更新器使用 `useCallback` 闭包捕获的 `searchParams` 快照,连续调用时第二次的 `prev` 还是第一次更新前的旧值。多参数同时更新必须使用单次 `setParams({...})` 批量操作。
|
||||
|
||||
### useConfirmAction
|
||||
|
||||
`src/web/shared/hooks/useConfirmAction.ts` — 包装异步操作,提供成功/失败 toast 通知。
|
||||
|
||||
```tsx
|
||||
const { confirmAction } = useConfirmAction();
|
||||
confirmAction(() => deleteMutation.mutateAsync(id), "删除成功");
|
||||
```
|
||||
|
||||
## 后端基础设施
|
||||
|
||||
### parseListParams
|
||||
|
||||
`src/server/helpers/list-params.ts` — 统一解析列表请求参数。
|
||||
|
||||
```ts
|
||||
export interface ParsedListParams {
|
||||
keyword?: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: SortOrder;
|
||||
}
|
||||
|
||||
export function parseListParams(
|
||||
url: URL,
|
||||
mode: RuntimeMode,
|
||||
options?: { allowedSortBy?: string[] },
|
||||
): ParsedListParams | Response;
|
||||
```
|
||||
|
||||
- 校验 page(正整数)、pageSize(正整数,最大 200)
|
||||
- 校验 sortBy(白名单,由调用方传入 `allowedSortBy`)
|
||||
- 校验 sortOrder(仅 "asc" / "desc")
|
||||
- 校验失败返回 400 Response(调用方应直接返回)
|
||||
- 替代了旧的 `validatePagination` 中间件
|
||||
|
||||
### 数据访问层约定
|
||||
|
||||
每个实体的 DB 函数(`listProjects` / `listModels` / `listProviders`):
|
||||
|
||||
- 通过 `paginateQuery` 工具执行分页查询
|
||||
- 接受 `sortBy` / `sortOrder` 参数,各实体自带白名单(`buildOrderBy`)
|
||||
- 默认排序:`desc(createdAt)`
|
||||
- 实体专用筛选参数在 DB 层处理(如 `status`、`type`、`capabilities`)
|
||||
|
||||
### 路由层约定
|
||||
|
||||
每个实体的列表路由:
|
||||
|
||||
```ts
|
||||
// src/server/routes/{entity}/list.ts
|
||||
const parsed = parseListParams(url, mode, { allowedSortBy: [...] });
|
||||
if (parsed instanceof Response) return parsed;
|
||||
// 解析实体专用筛选参数(如 status、providerId、capabilities、type)
|
||||
// 调用 DB 函数
|
||||
```
|
||||
|
||||
## URL 参数约定
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| -------------- | -------------- | ------ |
|
||||
| `page` | 当前页码 | 1 |
|
||||
| `pageSize` | 每页条数 | 20 |
|
||||
| `keyword` | 搜索关键词 | - |
|
||||
| `sortBy` | 排序列 | - |
|
||||
| `sortOrder` | 排序方向 | - |
|
||||
| `status` | 项目状态筛选 | - |
|
||||
| `type` | 供应商类型筛选 | - |
|
||||
| `providerId` | 模型供应商筛选 | - |
|
||||
| `capabilities` | 模型能力筛选 | - |
|
||||
|
||||
默认值不出现在 URL 中。
|
||||
|
||||
## 排序约定
|
||||
|
||||
- 排序列后端白名单校验,拒绝无效列名
|
||||
- 所有列排序通过后端 `ORDER BY` 实现,不使用前端排序
|
||||
- Table 组件的 `column.sorter` 设为 `true` 启用排序指示器
|
||||
- Table 的 `onChange` 回调统一处理分页 + 排序变更
|
||||
|
||||
## 交互约定
|
||||
|
||||
| 操作 | 触发时机 | 行为 |
|
||||
| -------- | -------------------- | ----------------------------- |
|
||||
| 筛选下拉 | 选中选项 / 清除 | 重置到第 1 页,更新 URL |
|
||||
| 搜索 | 点击搜索按钮 / Enter | 重置到第 1 页,更新 URL |
|
||||
| 重置 | 点击重置按钮 | 清除所有筛选条件,回到第 1 页 |
|
||||
| 分页 | 点击分页器 | 更新 URL(page+pageSize) |
|
||||
| 排序 | 点击列头排序 | 重置到第 1 页,更新 URL |
|
||||
| 删除 | Popconfirm 确认后 | toast 通知,刷新列表 |
|
||||
Reference in New Issue
Block a user