Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

编写组件/页面

定义完 Entity 后,就可以开始编写组件和页面了。Oak 的前端开发和常见 React 项目最大的不同在于:组件往往不是独立地“自己发请求”,而是挂在一棵数据树上,围绕对象路径来组织。

因此,写 Oak 组件时,建议先分清三层角色:

  • 页面 / panel 容器:负责组织路径、tab、弹窗、子组件编排;
  • 业务组件:负责某个对象的详情、编辑、列表、动作;
  • 抽象通用组件:只负责展示或编辑,不负责具体业务语义。

这一章后面的几个小节,会分别讲单个组件怎么写、组件之间怎么组织、OakComponent 有哪些参数和运行时能力。这里先给你一个整体视角。

先记住三类最常见的业务组件

对于关联到 Entity 的组件,最常见的还是这三类:

  • List:列表组件,显示当前对象的多条数据;
  • Detail:详情组件,显示当前对象的一条数据;
  • Upsert:编辑组件,编辑当前对象的一条数据,或为一条新数据准备 create。

如果一个组件不关联任何 Entity,那么它就是 Virtual 组件。Virtual 组件通常用来做:

  • 首页、看板、工作台;
  • 多个 Entity 混排的页面壳;
  • 纯布局、纯交互容器;
  • 只负责承载子组件的 panel / wrapper。

组件不是只分“页面”和“子组件”

在真实 Oak 项目里,组件通常还会再按职责分成下面几类:

类型常见命名作用
页面入口pages/...路由入口、挂页面根路径
panel 容器panel组织 tab、弹窗、多个子组件
业务详情组件detail展示一条对象数据
业务编辑组件upsert编辑一条对象数据
业务列表组件list / 具体名字管理一组对象
通用抽象组件oak-frontend-base/components/...纯展示/编辑壳,不带具体业务语义

例如在 oak-general-business 中:

  • system/panelSystem 的容器组件;
  • system/detail 展示并承接提交入口;
  • system/upsert 负责编辑 System
  • system/application 展示并管理属于某个 SystemApplication 列表。

application/panel 也采用了同样的模式:自己先取当前 Application 的核心数据,再把配置、样式、公众号能力、模板能力等拆成多个 tab 子组件。

这类 panel 组件,是 Oak 项目里非常值得掌握的一种组织方式。

优先复用的顺序

在 Oak 项目里,建议按下面这个顺序考虑复用,而不是一上来就自己写一套新组件。

1. 先看 oak-frontend-base 有没有抽象组件

oak-frontend-base/src/components 里有一批通用抽象组件,比如:

  • detail
  • upsert
  • list
  • filter
  • filterPanel
  • pagination
  • actionBtn
  • refAttr

这类组件的特点是:

  • 它们通常不代表某个具体业务;
  • 更像“对象展示器”“对象编辑器”“列表渲染器”;
  • 更适合拿来快速搭一个管理页、详情块、编辑块。

2. 再看 oak-general-business / oak-pay-business 有没有完整业务组件

如果你的需求已经落在公共业务包的职责范围里,优先复用它们现成的业务组件。例如:

  • SystemPanel
  • ApplicationPanel
  • 用户、登录、授权、消息、文件类组件;
  • 支付、账户、提现、物流类组件。

这些组件不仅有 UI,还通常已经把对象结构、路径组织、交互流程、动作提交一并处理好了。

3. 只有当公共组件不匹配时,再写项目私有组件

通常在下面几种情况下,才值得自己新写:

  • 现有公共组件的对象结构与你项目不一致;
  • 页面交互或展示方式有明显差异;
  • 你需要新增项目私有字段、动作或子组件关系;
  • 你需要一个专门的 panel 来组织项目内多个公共能力。

oak-frontend-base 抽象组件怎么用

这部分文档以前讲得还不够。这里直接按源码整理一份速查表。

抽象 detail 组件

源码入口:

oak-frontend-base/src/components/detail/index.ts

这类组件更像“详情展示壳”。它本身不负责对象取数,而是渲染父组件已经拿到的数据。

常用参数如下:

参数作用
entity当前展示的是哪个对象
attributes要展示哪些字段
data当前行数据
title标题
bordered是否带边框
layouthorizontalvertical
column列数,可按断点配置

适合场景:

  • 某个 panel 已经取到了当前对象数据;
  • 你只想快速把一组字段渲染成标准详情块;
  • 不想每次都手写 Descriptions / label 映射 / 枚举颜色逻辑。

抽象 upsert 组件

源码入口:

oak-frontend-base/src/components/upsert/index.ts

这类组件更像“表单编辑壳”,主要负责把字段定义转成输入控件,并通过 update(...) 写回当前结点。

常用参数如下:

参数作用
entity当前编辑的是哪个对象
attributes要编辑哪些字段
data当前行数据
layout表单布局
modedefaultcard
helps字段帮助文案

适合场景:

  • 你已经有当前对象上下文;
  • 只想快速把若干字段做成编辑表单;
  • 想复用字段类型推导、标签文案、枚举处理。

抽象 list 组件

源码入口:

oak-frontend-base/src/components/list/index.ts

它本质上是一个列表渲染器,而不是“自动帮你查询对象”的业务列表页。

常用参数如下:

参数作用
entity当前列表对应哪个对象
attributes列定义
data行数据数组
loading是否加载中
extraActions额外操作按钮
onAction行动作回调
rowSelection多选配置
hideHeader是否隐藏表头
disableSerialNumber是否禁用序号列
size表格尺寸
scroll滚动设置
empty空状态内容
opWidth操作列宽度

ListPro 不接受 tablePagination。它使用 Oak 自有的 Pagination 组件并从当前 list 结点读取分页状态;需要调整共享分页布局时,应修改或覆盖 Oak components/pagination.pagination 根样式,而不是配置 Ant Design Table 的分页参数。

适合场景:

  • 父组件已经通过 Oak 数据树拿到了行数组;
  • 你只想把这组行数据渲染成一个统一样式的列表;
  • 行上还需要配合 #oakLegalActions#oakLegalCascadeActions 展示动作按钮。

抽象 actionBtn 组件

源码入口:

oak-frontend-base/src/components/actionBtn/index.ts

这个组件不是“提交按钮”,而是动作按钮渲染器。它的职责是把当前行可执行动作、级联动作和额外动作,整理成一组按钮或下拉项。

常用参数如下:

参数作用
entity当前动作所属对象
actions当前行允许展示的动作
cascadeActions当前行允许展示的级联动作
extraActions额外自定义动作
onAction点击动作后的回调

适合场景:

  • 列表行操作区;
  • 详情页右上角动作区;
  • 需要同时展示普通动作和子对象动作的地方。

源码里还有两个值得注意的行为:

  • 普通动作和额外动作会一起参与排序和裁剪,超出的会进“更多”;
  • 级联动作的文案会沿路径解析目标 entity,再做 i18n 翻译。

如果你的页面已经通过 actions / cascadeActions 拿到了合法动作集合,actionBtn 往往是最省事的渲染方式。

抽象 filterPanel 组件

源码入口:

oak-frontend-base/src/components/filterPanel/index.ts

这个组件本质上是列表筛选条件面板,它本身不定义对象取数,而是帮你把筛选列转换成一组 named filters,并挂到当前 list 结点上。

常用参数如下:

参数作用
entity当前筛选面板对应的对象
columns要渲染哪些筛选项

适合场景:

  • 后台列表页;
  • 管理台侧边筛选区;
  • 需要把多个筛选条件统一折叠、展开、重置的列表页面。

这里有两个源码层面的真实约束:

  • 当前只支持一项全文检索列,也就是 columns 里最多一个 $text
  • 小程序端默认只展示前三个筛选项,其余会折叠进“更多筛选”。

另外,filterPanel 的重置和确认,最终调用的是当前结点上的:

  • removeNamedFilterByName(...)
  • refresh()

所以它最适合挂在已经建立好的 list 路径上,而不是脱离列表独立使用。

抽象 refAttr 组件

源码入口:

oak-frontend-base/src/components/refAttr/index.ts

这个组件主要用于外键字段展示和选择。如果某个字段本质上是“从别的对象里挑一条或多条”,而你不想每次都手写一套选择逻辑,它非常有用。

常用参数如下:

参数作用
placeholder占位文案
multiple是否多选
entityId单选当前值
entityIds多选当前值
pickerRender如何查询候选项、如何展示标题
onChange选择变化回调

pickerRender / pickerDef 实际会决定:

  • 候选对象是哪个 entity;
  • 用什么 projection 去取候选行;
  • 用什么 filter 限定候选范围;
  • 标题如何渲染;
  • 选择模式是 radioselect 还是别的形态。

源码里还有两个很实用的约束:

  • radio 模式要求候选总数 count <= 5
  • select 模式要求候选总数 count <= 20

这意味着它更适合:

  • 候选范围不大的外键选择;
  • 单选/多选对象引用;
  • 表单里的“选用户”“选系统”“选上级对象”。

如果候选集很大,通常应该改成专门的列表挑选页,而不是继续塞进 refAttr

抽象 pagination 组件

源码入口:

oak-frontend-base/src/components/pagination/index.ts

这个组件是列表分页状态的展示层,它依赖当前结点里的 oakPagination,并不会自己定义分页逻辑。

常用参数如下:

参数作用
entity当前分页对应的对象
showQuickJumper是否允许快速跳页
size分页器尺寸
showSizeChanger是否允许切换每页条数
showTotal自定义总数展示

适合场景:

  • 已经用 Oak list 组件建立了分页结点;
  • 你只需要一个分页器 UI 与当前结点联动。

需要注意的是:

  • 它依赖当前 list 结点上的 oakPagination
  • 因此应放在同一条 list 路径上下文中使用;
  • 如果列表根本没有配置分页或总数,这个组件也不会凭空帮你算出来。

抽象 search 组件

源码入口:

oak-frontend-base/src/components/search/index.ts

这个组件是列表关键字搜索辅助组件。它的核心能力不是渲染一个输入框本身,而是把关键字转换成名为 search 的 named filter,挂到当前 list 结点上。

常用参数如下:

参数作用
entity当前搜索针对哪个对象
attributes指定在哪些字段上搜索
placeholder搜索框占位文案

它的实际行为有两种:

  • 如果不传搜索字段,则默认生成全文检索 filter,即 $text.$search
  • 如果传了字段数组,则会生成一组 $or 条件,按字段路径分别构造搜索 filter。

适合场景:

  • 后台列表页顶部快速搜索;
  • 配合 filterPanel 形成“关键字 + 条件筛选”的组合检索;
  • 某些对象只希望暴露少数字段给用户搜索。

一个实践建议是:

  • 如果你的对象已经建立了全文索引,优先让 search 走全文检索;
  • 如果你只想允许用户搜少数字段,再显式传 attributes

抽象 filter 组件

源码入口:

oak-frontend-base/src/components/filter/index.ts

filterPanel 不同,filter单个筛选项组件。它负责把某一列的筛选定义,转换成具体控件和具体 filter。

常用参数如下:

参数作用
entity当前筛选项对应哪个对象
column这一项的筛选定义

从源码看,它会根据字段类型和操作符自动决定渲染方式,例如:

  • 数值、文本字段:输入框;
  • datetime:日期选择器;
  • booleanenum:选择器;
  • ref:引用对象选择器。

同时它会根据配置自动生成 named filter 名称,并在用户确认后调用:

  • addNamedFilter(...)
  • removeNamedFilterByName(...)

适合场景:

  • 你不想用整块 filterPanel,而是只想在页面某处插入一个单独筛选器;
  • 你想自定义筛选布局,但仍希望复用 Oak 的字段类型推导和 filter 构造逻辑。

抽象 picker 组件

源码入口:

oak-frontend-base/src/components/picker/index.ts

picker基于 list 结点的对象选择器。和 refAttr 相比,它更适合“打开一个候选列表让用户选”的场景,而不是直接把小范围候选塞进单个表单控件。

常用参数如下:

参数作用
entity候选对象属于哪个 entity
multiple是否多选
onSelect用户确认选择后的回调
title每行如何显示标题
titleLabel选择框标题
filter候选数据过滤条件
sorter候选数据排序
projection候选行需要取哪些字段

这个组件本身会建立一个 list 结点:

  • entity() 直接来自外部 props;
  • projection() 直接复用外部传入的 projection;
  • filters 直接把外部传入的 filter 接入当前结点。

适合场景:

  • 候选对象很多,refAttr 不够用;
  • 需要带筛选条件地选一条或多条对象;
  • 需要在弹窗、抽屉、独立 tab 中做“从对象列表里挑选”。

可以把它理解成:refAttr 更偏列表式选择,比自己手写一个 list + 选择逻辑更省事。

抽象 actionBtnPanel / actionTabPanel 组件

源码入口:

  • oak-frontend-base/src/components/actionBtnPanel/index.ts
  • oak-frontend-base/src/components/actionTabPanel/index.ts

这两个组件都属于动作集合展示壳,区别主要在布局:

  • actionBtnPanel 更像按钮面板 / 操作工具条;
  • actionTabPanel 更像分页签 / 卡片式动作网格。

actionBtnPanel 的常用参数:

参数作用
entity动作所属对象
items要展示的动作项
mode展示模式,如 defaultcelltable-cell
column一行显示多少项
fixed是否固定布局

actionTabPanel 的常用参数:

参数作用
entity动作所属对象
items要展示的动作项
rows每页几行
column每行几列
mode文本或其它展示模式

适合场景:

  • 首页或详情页上的统一动作区;
  • 小程序中的多动作入口面板;
  • 一组动作很多,需要分页或折叠展示。

这两个组件内部都会对动作项做一层统一处理:

  • 如果动作项有 label,优先使用自定义文案;
  • 否则优先翻译 entity 自身动作文案;
  • 再不行就退回 common::action.xxx

因此,当你已经整理好动作项数组时,这两个组件可以帮你省掉一层动作按钮布局代码。

抽象 listPro 组件

源码入口:

oak-frontend-base/src/components/listPro/index.tsx

如果说 list 更像“纯列表渲染器”,那么 listPro 更像 Oak 后台里最常见的标准列表页壳。它内部实际上是:

  • 上方 ToolBar
  • 中间 list
  • 配合 TableContext 管理列显示状态;
  • 默认把刷新动作绑定到当前 oakPath 对应结点上。

常用参数如下:

参数作用
entity当前列表对应哪个对象
attributes列定义
data行数据数组
title列表标题
extraContent工具条右侧附加内容
buttonGroup工具条按钮组
extraActions行额外动作
onAction行动作回调
oakPath当前列表路径,默认刷新会用到
rowSelection多选配置
disableSerialNumber是否关闭序号列
size / scroll / empty / opWidth表格显示控制
hideDefaultButtons是否隐藏默认工具条
onReload自定义刷新逻辑

这里有一个很实用的源码细节:

  • 如果你传了 oakPath,但没有自定义 onReloadlistPro 工具条上的刷新会直接调用 features.runningTree.refresh(oakPath)
  • 因此它最适合和 Oak list 结点放在同一路径上下文里使用。

适合场景:

  • 后台标准列表页;
  • 列表上方带标题、按钮、刷新入口;
  • 想复用 List + ToolBar 的统一外观,而不是每次手拼。

在真实项目中,这种用法非常常见。例如:

  • oak-pay-business/src/components/order/list/web.pc.tsx
  • taicang/src/pages/console/order/list/web.pc.tsx

都采用了 FilterPanel + ListPro 的组合。

抽象 pageHeader 组件

源码入口:

  • oak-frontend-base/src/components/pageHeader/index.ts

这个组件属于页面壳组件,主要负责:

  • 页头标题;
  • 返回按钮;
  • 页头右侧操作区;
  • 页面内容容器。

pageHeader 的常用参数:

参数作用
title页标题
subTitle副标题
extra页头右侧内容
tags标题旁标签
showBack / onBack / delta返回按钮控制
contentMargin内容区是否保留默认边距
contentStyle / contentClassName内容区样式
children页面主体内容

适合场景:

  • 后台管理页的统一页头;
  • 列表页、详情页、配置页的内容容器;
  • 你希望把“页面标题 + 筛选区 + 列表区”包在一个稳定的视觉外壳里。

taicanghaina-busi 里,这类页面壳的使用都非常多,尤其是:

  • 列表页最外层包 PageHeader
  • 页头内部先放筛选区;
  • 下方再放 ListPro 或详情内容。

关系权限管理相关组件

源码入口:

  • oak-frontend-base/src/components/relation/path/list
  • oak-frontend-base/src/components/relation/path/detail
  • oak-frontend-base/src/components/relation/path/upsert
  • oak-frontend-base/src/components/relation/actionAuth
  • oak-frontend-base/src/components/relation/relationAuth

这组组件不是通用页面壳,而是围绕系统实体 pathactionAuthrelationAuth 的专用管理组件。它们主要用于:

  • 配置对象路径;
  • 配置动作授权矩阵;
  • 配置关系授权矩阵。

对新手来说,先把它们理解成“系统管理后台专用组件”就够了。平时业务开发里,不建议把它们当成通用抽象组件到处复用;真正需要对象关系授权配置时,再顺着这几个目录去读源码会更合适。

再补一个实用建议:通用组件放哪一层

这些抽象通用组件,最常见的放置位置通常是:

组件推荐放置层
detaildetail / panel 子块
upsertupsert / 弹窗 / 步骤块
list业务 list 外壳内部
actionBtn列表操作列、详情页动作区
actionBtnPanel / actionTabPanel首页动作区、页头动作区、操作面板
searchlist 页顶部快速搜索区
filter自定义单筛选项区域
filterPanellist 页顶部或侧边筛选区
refAttr表单字段内部
picker弹窗/抽屉中的对象选择区
paginationlist 页底部
listPro后台标准列表页主体
pageHeader页面最外层壳

在项目里通常会再包一层 AbstractComponents

这也是实际项目里非常常见的一步。

taicangoak-pay-businesshaina-busi 这类项目,都会在自己的:

  • src/components/AbstractComponents.ts

里,把 oak-frontend-base 的抽象组件按本项目的 EntityDict 重新声明一遍,例如:

import AbsListPro from '@oak-frontend-base/components/listPro';
import { EntityDict } from '@project/oak-app-domain';

const ListPro = AbsListPro as <T extends keyof EntityDict>(
    ...props: Parameters<typeof AbsListPro<EntityDict, T>>
) => React.ReactElement;

这样做有三个直接好处:

  • 组件使用时会自动带上项目自己的 Entity 类型;
  • 页面里不用每次都手写一长串泛型;
  • 后续如果要统一替换或二次封装抽象组件,也有一个稳定入口。

如果你的项目已经进入“组件越来越多”的阶段,很建议尽早建立这层封装。

一个后台列表页的推荐拼法

如果你现在正在写一个后台列表页,最稳妥、也最接近真实项目的组合通常是:

  1. 最外层用 pageHeader 做页面壳;
  2. 页头或顶部区域放 filterPanel
  3. 主体区域放 listPro
  4. 行内新增、编辑再用弹窗挂 upsert
  5. 详情跳转或局部编辑再决定用共享路径、行路径或绝对路径。

一个非常典型的渲染结构大概像这样:

<PageHeader title={t('pageTitle')}>
    <FilterPanel
        entity="order"
        oakPath={oakFullpath}
        columns={filterColumns}
    />
    <ListPro
        entity="order"
        oakPath={oakFullpath}
        data={orders}
        attributes={attributes}
        extraActions={extraActions}
        onAction={handleAction}
    />
</PageHeader>

这里要特别注意一件事:

  • FilterPanelListPro 最好放在同一条 list 路径上;
  • 这样筛选条件、刷新动作、分页状态才会自然落在同一个 runningTree 结点里。

什么时候用抽象组件,什么时候自己写 OakComponent

可以用下面这个标准判断:

优先用抽象组件

当你只是需要:

  • 展示一条当前对象数据;
  • 编辑一条当前对象数据;
  • 渲染一组当前对象数据;
  • 做标准字段展示、标准字段编辑、标准表格列表。

优先自己写 OakComponent

当你需要:

  • 自己定义 projectionfilterssorterspagination
  • 自己组织 oakPath 和父子结点;
  • formData 中组合多段数据;
  • 自己决定 executeclean、弹窗、tab、步骤条等交互结构;
  • 把多个业务组件组装成一个 panel。

实际上,Oak 项目里最常见的写法不是“二选一”,而是:

  • 外层自己写一个业务 OakComponent
  • 内层在合适的位置复用 oak-frontend-base 抽象组件。

一个很典型的 panel 模式

如果你打开下面两个组件,会看到非常相似的结构:

  • oak-general-business/src/components/system/panel
  • oak-general-business/src/components/application/panel

它们的共同特点是:

  1. panel 自己先拿到当前对象的核心数据;
  2. 再通过 tab 或子区域,挂多个子组件;
  3. 子组件有的共享路径,有的走关联路径,有的走绝对路径;
  4. panel 自己不一定负责每个 tab 的细节,但负责整体编排。

这种模式非常适合:

  • 管理后台详情页;
  • 配置中心;
  • 一个对象下挂很多子能力的业务场景;
  • 支付、系统配置、公众号能力这种“一个对象,多块配置”的页面。

阅读顺序建议

为了避免一开始被路径和组件树绕晕,建议按下面顺序阅读后面的章节:

  1. 先看 编写详情组件
  2. 再看 编写更新组件
  3. 再看 编写列表组件
  4. 然后回到 组织组件
  5. 最后看 定义组件

这样先理解“单个组件怎么工作”,再理解“多个组件怎么组成页面”,会更顺。