Files
Alfred/docs/development/crud.md

5.9 KiB
Raw Blame History

CRUD 管理页面模式

管理类页面(项目管理、模型管理、供应商管理)遵循统一的 CRUD 模式。本文档描述该模式的后端和前端约定。

背景

三个管理页面原本各自实现了工具条、筛选、分页、搜索等逻辑,代码重复且交互不一致。统一后所有管理页面使用相同的基础设施。

页面结构

每个管理页面的组件结构:

<Space className="app-page-flex" orientation="vertical" size="large">
  <FilterToolbar />     ← 筛选 + 搜索 + 操作按钮
  <EntityTable />       ← 数据表格(分页 + 排序 + 行操作)
  <EntityFormModal />   ← 创建/编辑弹窗
</Space>

前端基础设施

FilterToolbar

src/web/shared/components/FilterToolbar.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 查询参数。

const { params, setParams, resetAll } = usePageSearchParams({
  defaults: { page: "1", pageSize: "20" },
});
  • paramsRecord<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 通知。

const { confirmAction } = useConfirmAction();
confirmAction(() => deleteMutation.mutateAsync(id), "删除成功");

后端基础设施

parseListParams

src/server/helpers/list-params.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 层处理(如 statustypecapabilities

路由层约定

每个实体的列表路由:

// 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 页
分页 点击分页器 更新 URLpage+pageSize
排序 点击列头排序 重置到第 1 页,更新 URL
删除 Popconfirm 确认后 toast 通知,刷新列表