# 管理端 CRUD 页面标准（CurdSchema）

CurdSchema 是**管理端专用**（`FrontAppSchema.type === 'admin'`）的 CRUD 页面标准：绑定一张实体表 + 一个管理端 app，描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页（+ 动作页）的生成规格。

页面定义文件按**管理端 app + 实体表**组织：`{project}/curd_schema/{app}/{table}.curd.ts`（一文件一 curd；同一张表可在不同 admin app 下分别 CRUD——curd 的身份是 **(app, table)**，生成物落在 `dto_schema/{app}/` 也按 app 隔离，不会互相撞名）。

**CurdSchema 只依赖 table schema（`Field` 实例），不挂钩 DTO（`DtoMessage`）**——DTO 由生成器按标准从 `columns` 推导。

## 定义

```ts
import { defineCurd } from '@pylonts/dsl';
import { admin } from '../project.config';
import { order } from '../schema/order.table';
import { merchant } from '../schema/merchant.table';
import { orderListFilter } from '../filter_schema/api/admin/filter/order-list.filter';

export const orderCurd = defineCurd('order', {   // name = table.name 的 kebab（即 admin 路由路径）
  description: '订单管理',
  app: admin,                      // 所属管理端（project.config.ts 的 FrontAppSchema 共享实例）
  table: order,                     // 绑定实体表（共享实例）
  title: '订单管理',
  section: '订单管理',               // 必填：sidebar 分组名
  actions: [defineAction('EXPORT', '导出订单')],   // 额外操作按钮
  actionPages: {
    add:    { mode: 'modal', columns: [order.columns.order_no, order.columns.mer_id] },
    update: { mode: 'modal', columns: [order.columns.id, order.columns.order_no] },
    detail: { mode: 'route',  columns: [order.columns.id, order.columns.order_no, order.columns.amount] },
  },
  list: {
    columns: [order.columns.id, order.columns.order_no, merchant.columns.name],  // 可跨表
    filter: orderListFilter,       // 搜索表单 + keyword（FilterSchema 引用，可选）
    orderBy: { column: order.columns.id, direction: 'desc' },
    columnTitles: { order_no: '订单号', name: '商户名称' },   // Field.name → 文案
  },
});
```

搜索条件**不内联在 list 里**，而是独立的 **FilterSchema**（`defineFilter`）声明，存放在 `filter_schema/{api.name}/{app.name}/filter/`（机器校验：一文件一 filter，文件名 = 名字去 Filter 后缀转 kebab）：

```ts
// filter_schema/api/admin/filter/order-list.filter.ts
import { defineFilter } from '@pylonts/dsl';
import { admin, api } from '../../../project.config';
import { order } from '../../../schema/order.table';

export const orderListFilter = defineFilter({
  name: 'OrderListFilter',
  api,
  app: admin,
  conditions: [
    { field: order.columns.status, optional: true },              // op 默认 eq；optional = 有值才加 WHERE
    { field: order.columns.order_no, op: 'like', optional: true },
  ],
  keyword: { columns: [order.columns.order_no] },                 // 单输入值多列 OR 模糊
});
```

生成物为 `{api}/src/modules/{app}/filter/{FilterName}.ts`（两段柯里化 WHERE 拼装方法，DAO/Service 列表查询共用）。

## 字段

| 字段 | 类型 | 说明 |
|---|---|---|
| `app` | `FrontAppSchema` | 所属管理端（共享实例，`type` 必须为 `'admin'`） |
| `table` | `TableSchema` | 绑定实体表（共享实例） |
| `title` | `string` | 列表页中文标题 |
| `section` | `string` | **必填**：sidebar 分组名 |
| `actions?` | `ActionSchema[]` | 页面额外可执行动作（标准 CRUD 之外，如导出、审核） |
| `actionPages?` | `{ add? / update? / detail? }` | 动作页：`{ mode: 'modal' \| 'route'; columns: Field[] }` |
| `list` | `CurdListConfig` | 列表页配置（必填） |

### ActionPage

| 字段 | 类型 | 说明 |
|---|---|---|
| `mode` | `'modal' \| 'route'` | 弹窗或独立路由 |
| `columns` | `Field[]` | 该页面渲染的字段，**必填非空**——前端要显示的字段必须全部显式列出 |

### CurdListConfig

| 字段 | 类型 | 说明 |
|---|---|---|
| `columns` | `Field[]` | 列表列，**必填非空**；可含跨表字段 |
| `filter?` | `FilterSchema` | 页面过滤器引用：搜索表单（AND 条件）+ keyword（多列 OR 模糊）；缺省 = 无搜索表单 |
| `orderBy` | `{ column: Field; direction: 'asc' \| 'desc' }` | 默认排序，**必填**，column 与 direction 都必填；column 必须是**本表字段实例** |
| `columnTitles?` | `Record<string, string>` | 列标题覆盖：`Field.name` → 中文文案 |

### FilterSchema（`defineFilter`）

| 字段 | 类型 | 说明 |
|---|---|---|
| `name` | `string` | PascalCase、`Filter` 结尾；导出名 = name 首字母小写 |
| `api` | `ProjectApiSchema` | 所属后端 api（project.config.ts 共享实例）；`api.apps` 必须包含 `app` |
| `app` | `FrontAppSchema` | 所属前端 app（共享实例）；必须与引用它的 curd 同 app |
| `conditions?` | `FilterCondition[]` | AND 组合条件：`{ field, op?='eq', right?, optional? }`；`optional: true` = 有值才加 WHERE（页面搜索场景） |
| `keyword?` | `{ columns: Field[] }` | 单输入值对多列 OR like 模糊；配置后驱动「关键词查询」端点（`query({ keyword })`，供 Select/AutoComplete 搜索） |

## 跨表字段

`list.columns` 与 filter `conditions` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示/过滤关联表字段（如订单列表显示商户名称、按商户名称过滤）。

## 默认与校验

- `list.columns` / `actionPages.*.columns` **必填非空**（不允许省略、不允许空数组）
- `list.orderBy` **必填**，`column` 与 `direction` 都必填（规格：默认主键 desc 由定义方显式写出）
- 运行时校验（`defineCurd`，仿 `defineTable` 强校验风格）：
  - `app.type` 必须为 `'admin'`，否则抛错
  - **`name` 必须是 `table.name` 的 kebab 形式**（name 即 admin 路由路径，不允许与所服务的表漂移）
  - `section` 必填
  - 所有 `columns` 非空，否则抛错
  - `list.filter.app` 必须 === `curd.app`，否则抛错
  - `list.orderBy.column` 必须属于 `table`，否则抛错
  - `list.columns` 允许跨表，**不校验归属**
- 存储校验（`loadCurds`，仿 `loadDaos`/`loadEntities` 机器校验风格）：
  - 唯一合法目录是 `curd_schema/{app.name}/`（一级，app 名）；`curd.app` 必须是 project.config.ts 共享实例（`type === 'admin'`），且与所在目录一致
  - **文件名 = `{table}.curd.ts`（表名 verbatim**，与 `{table}.entity.ts` / `{table}.dao.ts` 同规）——(app, table) 两维决定 curd 身份，同一张表可在不同 admin app 下合法共存
  - 一文件一 curd（多导出/零导出报错）
- 运行时校验（`defineFilter`）：`api.apps` 包含 `app`；conditions 与 keyword 不能同时为空；keyword.columns 非空
- 生成时校验（curd 生成器，`DtoSchemaGen.add` / `update`）：
  - 表配置 `autoIncrement` 或 `generator`（主键由服务端生成）时，`actionPages.add.columns` **不允许包含主键字段**，否则抛错——AddRequest 不携带服务端生成的主键
  - `actionPages.update.columns` **必须包含主键字段**，否则抛错——UpdateRequest 靠主键定位记录

## DTO 推导（生成器约定）

DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导，页面语义不持有 DTO 实例：

| DTO | 命名 | 字段来源 |
|---|---|---|
| Row | `{Pascal}Row` | `list.columns` |
| ListRequest | `{Pascal}ListRequest` | filter 的 conditions（camelCase + op）与 keyword + 分页参数（`PageRequest`，仅 paginated 表） |
| QueryRequest | `{Pascal}QueryRequest` | filter 的 conditions + keyword，无分页——keyword 查询端点专用（仅配置 keyword 时生成） |
| ListResponse | `{Pascal}ListResponse` | `PageResult(Row)`（仅 paginated 表；非分页表列表接口直接返回 `Row[]`，不生成 ListResponse） |
| AddRequest | `{Pascal}AddRequest` | `actionPages.add.columns` |
| UpdateRequest | `{Pascal}UpdateRequest` | `actionPages.update.columns` |
| DetailRequest | `{Pascal}DetailRequest` | 主键 |
| DetailResponse | `{Pascal}DetailResponse` | `actionPages.detail.columns` |

## 与旧 PageConfig 的差异

| PageConfig（旧方案，已废弃） | CurdSchema |
|---|---|
| `module: string` | 由 `app` 推导（后端模块 == app 1:1） |
| `schema: 'bd'` 字符串 | `table: TableSchema` 实例（类型安全） |
| `operations: { label, action }` | `actions: ActionSchema[]` |
| `detail.mode` 单例 | `actionPages.detail.mode` |
| `forms.add / forms.update` | `actionPages.add / actionPages.update` |
| `keyword` / `orderBy` / `columnTitles` | `list.filter`（FilterSchema）/ `list.orderBy` / `list.columnTitles` |
| `naming` | 去掉（DTO 命名是生成器约定，非页面语义） |
| DTO 引用（`request` / `fields` / `DtoFields`） | 去掉（DTO 由生成器推导，页面只依赖 table） |