refactor: 统一管理页面布局 — FilterToolbar + usePageSearchParams + parseListParams
This commit is contained in:
@@ -5,14 +5,15 @@
|
||||
## 目录索引
|
||||
|
||||
```text
|
||||
docs/
|
||||
README.md
|
||||
development/
|
||||
docs/
|
||||
README.md
|
||||
architecture.md
|
||||
backend.md
|
||||
frontend.md
|
||||
release.md
|
||||
development/
|
||||
README.md
|
||||
architecture.md
|
||||
backend.md
|
||||
crud.md
|
||||
frontend.md
|
||||
release.md
|
||||
user/
|
||||
README.md
|
||||
usage.md
|
||||
@@ -39,18 +40,18 @@ docs/
|
||||
|
||||
## 按任务阅读路径
|
||||
|
||||
| 任务 | 必读文档 |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| 修改项目介绍或快速开始 | [项目 README](../README.md)、本文档 |
|
||||
| 修改开发流程、质量门禁或工程规则 | [开发文档](development/README.md)、本文档、[OpenSpec 配置](../openspec/config.yaml) |
|
||||
| 修改架构边界或启动流程 | [开发文档](development/README.md)、[架构与边界](development/architecture.md) |
|
||||
| 修改后端 API、配置加载、日志 | [开发文档](development/README.md)、[后端开发](development/backend.md) |
|
||||
| 修改前端 | [开发文档](development/README.md)、[前端开发](development/frontend.md) |
|
||||
| 修改构建、脚本、发布 | [构建与发布](development/release.md)、[部署文档](user/deploy.md) |
|
||||
| 修改配置 schema | [配置文件](user/config.md)、[后端开发](development/backend.md) |
|
||||
| 修改文档规则或文档目录结构 | 本文档、[OpenSpec 配置](../openspec/config.yaml) |
|
||||
| 首次安装或配置 | [快速开始](user/usage.md)、[配置文件](user/config.md) |
|
||||
| 排查运行或构建问题 | [故障排查](user/troubleshoot.md) |
|
||||
| 任务 | 必读文档 |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| 修改项目介绍或快速开始 | [项目 README](../README.md)、本文档 |
|
||||
| 修改开发流程、质量门禁或工程规则 | [开发文档](development/README.md)、本文档、[OpenSpec 配置](../openspec/config.yaml) |
|
||||
| 修改架构边界或启动流程 | [开发文档](development/README.md)、[架构与边界](development/architecture.md) |
|
||||
| 修改后端 API、配置加载、日志 | [开发文档](development/README.md)、[后端开发](development/backend.md) |
|
||||
| 修改前端 CRUD 管理页面 | [开发文档](development/README.md)、[前端开发](development/frontend.md)、[CRUD 模式](development/crud.md) |
|
||||
| 修改构建、脚本、发布 | [构建与发布](development/release.md)、[部署文档](user/deploy.md) |
|
||||
| 修改配置 schema | [配置文件](user/config.md)、[后端开发](development/backend.md) |
|
||||
| 修改文档规则或文档目录结构 | 本文档、[OpenSpec 配置](../openspec/config.yaml) |
|
||||
| 首次安装或配置 | [快速开始](user/usage.md)、[配置文件](user/config.md) |
|
||||
| 排查运行或构建问题 | [故障排查](user/troubleshoot.md) |
|
||||
|
||||
## 文档归属矩阵
|
||||
|
||||
@@ -63,6 +64,7 @@ docs/
|
||||
| 架构边界、启动流程、运行时流程、前后端边界 | `docs/development/architecture.md` |
|
||||
| 后端模块 API、工具函数索引、数据库 schema、AI 层实现 | `docs/development/backend.md` |
|
||||
| 前端运行时代码结构、组件索引、页面组成、hooks/工具清单 | `docs/development/frontend.md` |
|
||||
| 管理页面 CRUD 模式(筛选工具条、URL 同步、分页排序约定) | `docs/development/crud.md` |
|
||||
| 构建、发布、脚本、前后端静态资源集成 | `docs/development/release.md` |
|
||||
| 快速开始、安装配置 | `docs/user/usage.md` |
|
||||
| YAML 配置、变量语法、server/storage/logging、JSON Schema | `docs/user/config.md` |
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
- `response.ts`:`createApiError(error, status)`、`createHeaders(mode, init)`、`createMetaResponse(version)`、`formatDuration(ms)`、`jsonResponse(body, options)`
|
||||
- `url.ts`:`parseIdFromUrl(url)`
|
||||
- `list-params.ts`:`parseListParams(url, mode, options?)` — 统一校验分页/排序参数,替代 validatePagination
|
||||
- `pagination.ts`:`paginateQuery()` — Drizzle 分页查询封装
|
||||
|
||||
`src/server/middleware/`:
|
||||
|
||||
@@ -97,4 +99,4 @@ SQLite + bun:sqlite + Drizzle ORM。
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改后端模块 API、共享工具、数据库 schema、AI 服务层或聊天 API 时,必须更新本文档。
|
||||
修改后端模块 API、共享工具、数据库 schema、AI 服务层、聊天 API 或列表查询参数解析时,必须更新本文档。管理页面 CRUD 通用模式的详细约定见 [crud.md](crud.md)。
|
||||
|
||||
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 通知,刷新列表 |
|
||||
@@ -18,21 +18,21 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si
|
||||
| 功能模块 | 路径 | 说明 |
|
||||
| -------- | --------------------- | --------------------------- |
|
||||
| 仪表盘 | `features/dashboard/` | 总览页面 |
|
||||
| 项目管理 | `features/projects/` | 项目 CRUD、归档、搜索 |
|
||||
| 项目管理 | `features/projects/` | 项目 CRUD、归档、搜索、排序 |
|
||||
| 模型管理 | `features/models/` | 供应商/模型管理、连通性测试 |
|
||||
| 聊天 | `features/chat/` | 会话管理、消息渲染、AI 对话 |
|
||||
| 收集箱 | `features/inbox/` | 素材 CRUD、持久化、列表管理 |
|
||||
|
||||
## 页面
|
||||
|
||||
| 页面 | 路径 | 入口 |
|
||||
| -------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 总览 | `/` | `features/dashboard/index.tsx` |
|
||||
| 项目管理 | `/projects` | `features/projects/index.tsx` — ProjectToolbar(Tab 切换 active/archived + 搜索 + 新建) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除,仅 active 项目可跳转工作台。 |
|
||||
| 模型管理 | `/models` 和 `/models/providers` | 独立路由页面:`ModelListPage.tsx` + `ProviderListPage.tsx`。模型表单和表格使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`),新建时 type 默认 `openai-compatible`,测试 `ok: false` 展示失败但不阻止保存。 |
|
||||
| 聊天室 | `/workbench/:id` | `features/chat/index.tsx` |
|
||||
| 收集箱 | `/workbench/:id/inbox` | `features/inbox/index.tsx` — 协调层(selectedId + modalOpen)+ MaterialSidebar(列表容器)+ MaterialDetailPanel(详情容器)+ AddMaterialModal。素材 CRUD 通过 TanStack Query hooks 接入后端 API。 |
|
||||
| 404 | `*` | `features/not-found/index.tsx` |
|
||||
| 页面 | 路径 | 入口 |
|
||||
| -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 总览 | `/` | `features/dashboard/index.tsx` |
|
||||
| 项目管理 | `/projects` | `features/projects/index.tsx` — FilterToolbar(状态 Select + 搜索 + 新建/归档恢复删除) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除、列表排序、URL 同步筛选参数。 |
|
||||
| 模型管理 | `/models` 和 `/models/providers` | 独立路由页面:`ModelListPage.tsx`(FilterToolbar + ModelTable) + `ProviderListPage.tsx`(FilterToolbar + ProviderTable)。模型支持供应商/能力筛选和列表排序,供应商支持类型筛选和列表排序。模型表单使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`)。 |
|
||||
| 聊天室 | `/workbench/:id` | `features/chat/index.tsx` |
|
||||
| 收集箱 | `/workbench/:id/inbox` | `features/inbox/index.tsx` — 协调层(selectedId + modalOpen)+ MaterialSidebar(列表容器)+ MaterialDetailPanel(详情容器)+ AddMaterialModal。素材 CRUD 通过 TanStack Query hooks 接入后端 API。 |
|
||||
| 404 | `*` | `features/not-found/index.tsx` |
|
||||
|
||||
### 聊天页面
|
||||
|
||||
@@ -46,40 +46,44 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si
|
||||
|
||||
### 共享组件
|
||||
|
||||
| 组件 | 路径 | 说明 |
|
||||
| ------------- | ------------------------------------- | ------------------------------------ |
|
||||
| ConsoleShell | `shared/components/ConsoleShell/` | 全局布局外壳(Provider + Layout) |
|
||||
| Sidebar | `shared/components/Sidebar/` | 侧边栏纯展示组件 |
|
||||
| SidebarGroup | `shared/components/SidebarGroup/` | 可折叠日期分组(聊天室和收集箱共用) |
|
||||
| ErrorBoundary | `shared/components/ErrorBoundary.tsx` | 生产环境错误边界 |
|
||||
| 组件 | 路径 | 说明 |
|
||||
| ------------- | ------------------------------------- | ------------------------------------------------------ |
|
||||
| ConsoleShell | `shared/components/ConsoleShell/` | 全局布局外壳(Provider + Layout) |
|
||||
| FilterToolbar | `shared/components/FilterToolbar.tsx` | 统一筛选工具条(Select 筛选 + 搜索 + 重置 + 操作按钮) |
|
||||
| Sidebar | `shared/components/Sidebar/` | 侧边栏纯展示组件 |
|
||||
| SidebarGroup | `shared/components/SidebarGroup/` | 可折叠日期分组(聊天室和收集箱共用) |
|
||||
| ErrorBoundary | `shared/components/ErrorBoundary.tsx` | 生产环境错误边界 |
|
||||
|
||||
### 共享 Hooks
|
||||
|
||||
| Hook | 路径 | 说明 |
|
||||
| ----------------------- | --------------------------------------- | ---------------------------------------------------------- |
|
||||
| `use-meta.ts` | `shared/hooks/use-meta.ts` | `/api/meta`(30s 轮询,5s staleTime) |
|
||||
| `use-providers.ts` | `shared/hooks/use-providers.ts` | 供应商 CRUD + test connection |
|
||||
| `use-models.ts` | `shared/hooks/use-models.ts` | 模型 CRUD + test connection |
|
||||
| `use-projects.ts` | `shared/hooks/use-projects.ts` | 项目 CRUD + archive/restore |
|
||||
| `use-conversations.ts` | `shared/hooks/use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) |
|
||||
| `use-logger` | `shared/hooks/use-logger.ts` | Logger hook(组件内使用) |
|
||||
| `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 |
|
||||
| `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 |
|
||||
| `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 |
|
||||
| `use-current-project` | `shared/hooks/use-current-project.ts` | 当前工作台项目 + ProjectContext(需在 ProjectProvider 内) |
|
||||
| `use-materials.ts` | `shared/hooks/use-materials.ts` | 素材 CRUD(create/delete/fetch/list + Query hooks) |
|
||||
| Hook | 路径 | 说明 |
|
||||
| ------------------------ | --------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `use-page-search-params` | `shared/hooks/usePageSearchParams.ts` | URL 查询参数同步(筛选/分页/排序),批量更新 `setParams` 避免闭包覆盖 |
|
||||
| `use-confirm-action` | `shared/hooks/useConfirmAction.ts` | 包装异步操作 + toast 成功/失败通知 |
|
||||
| `use-meta.ts` | `shared/hooks/use-meta.ts` | `/api/meta`(30s 轮询,5s staleTime) |
|
||||
| `use-providers.ts` | `shared/hooks/use-providers.ts` | 供应商 CRUD + test connection |
|
||||
| `use-models.ts` | `shared/hooks/use-models.ts` | 模型 CRUD + test connection |
|
||||
| `use-projects.ts` | `shared/hooks/use-projects.ts` | 项目 CRUD + archive/restore |
|
||||
| `use-conversations.ts` | `shared/hooks/use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) |
|
||||
| `use-logger` | `shared/hooks/use-logger.ts` | Logger hook(组件内使用) |
|
||||
| `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 |
|
||||
| `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 |
|
||||
| `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 |
|
||||
| `use-current-project` | `shared/hooks/use-current-project.ts` | 当前工作台项目 + ProjectContext(需在 ProjectProvider 内) |
|
||||
| `use-materials.ts` | `shared/hooks/use-materials.ts` | 素材 CRUD(create/delete/fetch/list + Query hooks) |
|
||||
|
||||
### 共享工具函数
|
||||
|
||||
| 文件 | 导出 |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `utils/api.ts` | `handleResponse(response, extract)`、`handleVoidResponse(response)` |
|
||||
| `utils/format.ts` | `formatDatetime(iso: string)` — 格式化 ISO 时间字符串为 `YYYY-MM-DD HH:mm` |
|
||||
| `utils/time.ts` | `formatCountdown`、`formatDurationUnit`、`formatRelativeTime`、`isOlderThan`、`subtractHours` |
|
||||
| `utils/date-group.ts` | `getDateGroup`、`groupByDate`、`GROUP_LABELS`、`GROUP_ORDER`、`DateGroup`、`DateGroupData` |
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改前端技术栈、组件边界、数据流、样式规则、测试环境、前端验证方式、运行时代码结构、页面组成、组件索引、hooks/工具清单、目录结构或功能模块归属时,必须更新本文档。
|
||||
修改前端技术栈、组件边界、数据流、样式规则、测试环境、前端验证方式、运行时代码结构、页面组成、组件索引、hooks/工具清单、目录结构或功能模块归属时,必须更新本文档。管理页面 CRUD 通用模式的详细约定见 [crud.md](crud.md)。
|
||||
|
||||
## 日志模块
|
||||
|
||||
|
||||
Reference in New Issue
Block a user