Back to Nocobase

client-v2 components

packages/core/client-v2/src/components/README.zh-CN.md

2.1.019.7 KB
Original Source

client-v2 components

这里收纳 @nocobase/client-v2 暴露给业务插件复用的一组 React 组件。组件按目录组织——目前主要是 form/,给设置页和表单场景用。

写新插件前先翻一遍这份说明,能省下不少重复造轮子的功夫。组件之间互相耦合很少,按需 import 就行。

form/

form/ 目录下的组件围绕「设置页 + 表单」这一类场景。常见用法是配合 ctx.viewer.drawer / ctx.viewer.dialog 打开一个表单容器,里面放 antd 的 Form + Form.Item,字段用这里提供的标准控件。

下面按用途分四组:表单容器、表单字段、数据表、工具。

表单容器

DrawerFormLayout

抽屉形态的表单 layout。配合 ctx.viewer.drawer({ closable: true, content }) 用。

  • 顶部 Header:只放标题;左侧的关闭 X 来自 antd Drawer——必须在 viewer.drawer 上显式传 closable: true 才会出现
  • 底部 Footer:默认 Cancel / Submit 两个按钮;可以用 footer 完全替换
  • 中间 children:调用方自己放 <Form> 实例 + 字段
tsx
import { DrawerFormLayout } from '@nocobase/client-v2';

ctx.viewer.drawer({
  width: '50%',
  closable: true,  // 关键:开启 antd Drawer 原生关闭 X
  content: () => (
    <DrawerFormLayout
      title={t('添加认证器')}
      onSubmit={handleSubmit}
      submitting={submitting}
    >
      <Form form={form} layout="vertical">
      </Form>
    </DrawerFormLayout>
  ),
});

主要属性:

  • title:标题节点
  • onSubmit:回调,resolve 后会自动关闭抽屉。throw 可以让抽屉保持打开(比如校验失败)
  • submitting:驱动 Submit 按钮的 loading
  • submitText / cancelText:按钮文字
  • footer:完全自定义 Footer 内容(覆盖默认两个按钮)

需要在关闭前做「未保存改动」之类的确认,用更底层的 viewer.drawer({ preventClose, beforeClose }),这层 layout 不再包装 cancel 拦截。

DialogFormLayout

弹窗形态的表单 layout,跟 DrawerFormLayout 同形。配合 ctx.viewer.dialog({ closable: true, content }) 用。

视觉上的差异只有关闭 X 的位置——Drawer 是 antd Drawer 自带的左上角 X,Dialog 是 antd Modal 自带的右上角 X。两边都依赖在 viewer 调用处显式传 closable: true,layout 自己都不渲染 close 图标。

tsx
import { DialogFormLayout } from '@nocobase/client-v2';

ctx.viewer.dialog({
  closable: true,  // 关键:开启 antd Modal 原生右上角 X
  content: () => (
    <DialogFormLayout title={t('绑定验证码')} onSubmit={handleSubmit}>
      <Form form={form} layout="vertical">
      </Form>
    </DialogFormLayout>
  ),
});

什么时候选哪个?

  • Drawer:长表单、字段多、需要从一侧滑出占用整面(比如设置页的「添加 / 编辑」)
  • Dialog:短表单、需要快速确认(比如绑定、修改密码、二次验证)

属性跟 DrawerFormLayout 基本一致,可以直接换。唯一区别:DialogFormLayout 多一个 onCancel 回调(Cancel 按钮和原生 X 都会触发),用于「丢弃改动」之类的确认。

表单字段

RemoteSelect

异步拉数据的 Select。框架级组件——不感知 NocoBase 业务,调用方传一个 request 函数自己拉数据。

tsx
import { RemoteSelect } from '@nocobase/client-v2';

<Form.Item name="provider" label={t('服务商')}>
  <RemoteSelect<{ name: string; title: string }>
    request={async () => {
      const response = await ctx.api.resource('smsOTPProviders').list();
      return response?.data?.data || [];
    }}
    cacheKey="@nocobase/plugin-verification:smsOTPProviders:list"
    mapOptions={(item) => ({ label: compileT(item.title), value: item.name })}
  />
</Form.Item>

主要属性:

  • request: () => Promise:拉数据,必填。可以返回数组,也可以返回带元数据的对象(搭配 selectItems 取出数组)
  • selectItems:从 request 返回值中抽出数组的函数。响应体是 { items, meta } 形态时用
  • fieldNames:默认按 { label, value } 字段映射;不匹配的时候用 mapOptions 完全自定义
  • mapOptions: (item, index) => ({ label, value }):完整覆盖映射逻辑
  • cacheKey / refreshDeps / ready:透传给 ahooks useRequest,控制缓存和 refresh 时机
  • onLoaded: (items, response) => void:拉到数据后的回调,能拿到原始响应

剩下的 antd Select props(mode / placeholder / disabled / value / onChange 等)都原样透传。

默认开启 showSearch + allowClear,搜索是本地模式(按 label 匹配)。要做服务端搜索,把搜索词放进 refreshDeps,在 request 里读出来发请求。

EnvVariableInput

$env 命名空间的变量输入器。专门给「密钥 / 凭证」这类字段用——支持环境变量引用,同时给纯字面值加 password mask。

tsx
import { EnvVariableInput } from '@nocobase/client-v2';

<Form.Item name={['options', 'accessKeySecret']} label={t('Access Key Secret')}>
  <EnvVariableInput password />
</Form.Item>

主要属性:

  • password:开启后,非变量的字面值会用 Input.Password 形态遮盖。变量表达式(比如 {{ $env.X }})依然可见可编辑
  • placeholder / disabled / value / onChange:标准受控字段属性

值的形态是字符串:'literal''{{ $env.foo.bar }}'。服务端在使用时再展开成实际值。

VariableInput / VariableTextArea

通用变量输入器。可以引用任意 flowEngine.context 注册的命名空间($env / $user / 业务自定义的 $resetLink 等)。

两者差异:

  • VariableInput:单行,变量渲染成彩色 pill(视觉上是「短标签」)
  • VariableTextArea:多行,变量保留 {{ ... }} 字面——适合邮件模板这种「字面 + 变量」混排的长文本
tsx
import { VariableInput, VariableTextArea } from '@nocobase/client-v2';

// 邮件主题:单行 pill 形态
<Form.Item name={['options', 'emailSubject']} label={t('主题')}>
  <VariableInput
    namespaces={['$env']}
    extraNodes={[
      { name: '$resetLink', title: t('重置密码链接'), type: 'string', paths: ['$resetLink'] },
    ]}
  />
</Form.Item>

// 邮件正文:多行字面
<Form.Item name={['options', 'emailContentHTML']} label={t('正文')}>
  <VariableTextArea namespaces={['$env']} rows={10} />
</Form.Item>

主要属性:

  • namespaces:限定可选的顶层命名空间。不传就用 flowEngine.context 里全部已注册的
  • extraNodes:在命名空间过滤后追加几条静态变量(用于 $resetLink 这类只在当前页面有意义的局部变量)
  • converters:覆盖默认的 path ↔ string 转换器。EnvVariableInput 就是用这个钩子把输出锁定到 $env
  • delimiters:变量在存储字符串里使用的开闭分隔符,默认 ['{{', '}}'](对应 Handlebars 的 HTML 转义形式)。若字段最终以 HTML 渲染、转义会破坏变量内容(如站内信正文),传 ['{{{', '}}}'] 走 Handlebars 的原样输出形式
  • value / onChange / placeholder / disabled:标准受控字段属性

底层共用 VariableHybridInputVariableInput)和 TextAreaWithContextSelectorVariableTextArea),用同一套 MetaTree 数据。

TypedVariableInput

类型化常量 + 变量混合输入器。移植 v1 Variable.InputuseTypedConstant 形态:右侧斜体 x 按钮触发 Cascader 切换 [空值 | 常量<types> | 变量和密钥<…namespaces>],左侧根据当前模式渲染对应编辑器(Input / InputNumber / Select(True/False) / DatePicker)或一颗带变量路径的 pill。

用于字段同时接受字面量变量引用的场景。最典型的就是 plugin-notification-email 的 SMTP portsecure:可以填具体数字 / 布尔值,也可以填 {{ $env.SMTP_PORT }} 走环境变量。

tsx
import { TypedVariableInput } from '@nocobase/client-v2';

// 端口:数字常量 + $env 变量
<Form.Item name={['options', 'port']} label={t('端口')} initialValue={465}>
  <TypedVariableInput
    types={[['number', { min: 1, max: 65535, step: 1 }]]}
    namespaces={['$env']}
  />
</Form.Item>

// 安全模式:布尔常量 + $env 变量
<Form.Item name={['options', 'secure']} label={t('安全模式')} initialValue={true}>
  <TypedVariableInput types={['boolean']} namespaces={['$env']} />
</Form.Item>

主要属性:

  • types:允许的常量类型。形态对齐 v1 useTypedConstant,可以传裸类型名 ['number', 'boolean'],也可以传 [type, editorProps] 元组 [['number', { min, max, step }]] 把 props 透传给底层 antd 编辑器。默认 ['string', 'number', 'boolean', 'date']即使只允许一种类型,「常量」入口也会展开二级菜单(数字 / 逻辑值 / 日期 / 字符串)——跟 v1 一致,让用户能直观看到当前常量是什么类型
  • namespaces:限定变量 picker 可选的顶层命名空间(如 ['$env'])。不传就用 flowEngine.context 里所有已注册命名空间
  • extraNodes:在命名空间过滤后追加几条静态变量节点
  • nullable:是否暴露「空值」入口,默认 true。配合 Form.Item.rules={[{ required: true }]} 可以让用户能手动清空、但提交时会被校验拦截——跟 v1 的「空值 + required」组合一致
  • delimiters:变量 token 开闭分隔符,默认 ['{{', '}}'],跟 VariableInput 一致
  • value / onChange / placeholder / disabled / style / className:标准受控字段属性

值的形态:

  • 常量:原生类型直接存(number / boolean / Date / string
  • 变量:字符串 '{{ $env.SMTP_PORT }}'
  • 空值:null

什么时候不该用

  • 纯字面量字段(用户不会想填变量)→ 直接用 antd InputNumber / Select / DatePicker / Input,省掉 Cascader 那一格的视觉开销
  • 纯变量字段(用户不会想填字面量)→ 用 EnvVariableInput$env 专用,带 password mask)或 VariableInput(更通用)

跳过的能力(v1 有但 v2 还没补):

  • object 类型(JSON 编辑器)——v2 还没对应的「内联 JSON 编辑器 + Cascader 切换」组件,等真有需求再补
  • 异步 loadChildren 分支——大多数命名空间的 MetaTree 已经由 useFilteredMetaTree 提前展平,没遇到刚需

FileSizeInput

文件大小输入器。值统一存字节数,UI 上配一个单位选择器(Byte / KB / MB / GB)。

tsx
import { FileSizeInput } from '@nocobase/client-v2';

<Form.Item name="maxFileSize" label={t('单文件大小上限')}>
  <FileSizeInput min={1} max={1024 * 1024 * 1024} defaultValue={20 * 1024 * 1024} />
</Form.Item>

主要属性:

  • min / max:允许的字节数区间,blur 时会自动 clamp 回界内。默认 min=1max=Infinity
  • defaultValue:用来决定初次显示的单位(比如默认 20 MB 就会以 MB 单位起始)
  • value / onChange:受控字段,值类型是 number(字节)

PasswordInput

antd Input.Password 加一个可选的强度提示条,从 v1 的 Password 组件移植过来。用于「设置 / 修改密码」类表单——v1 → v2 迁移过来后视觉信号保持一致。

tsx
import { PasswordInput } from '@nocobase/client-v2';

<Form.Item name="newPassword" label={t('新密码')} rules={[{ required: true }]}>
  <PasswordInput autoComplete="new-password" checkStrength />
</Form.Item>

主要属性:

  • checkStrength:在输入框下方渲染一条强度提示。默认 false。强度评分按 [20, 40, 60, 80, 100] 分桶,用裁剪的橙色渐变填充在灰色底条上,配色跟 v1 保持一致
  • 其他 antd Input.Password 属性原样透传:value / onChange / disabled / placeholder / autoComplete

强度条只是 UX 提示,不是表单校验。弱密码仍然能提交,除非 server(或单独安装的 password-policy 商业插件)拒绝。真正的密码规则通过 Form.Item.rules 或——等开源 ↔ 商业的 extension point 落地之后——项目共享的 password validator hook 接入。

JsonTextArea

JSON 输入器。存的值是 JS 对象(不是字符串),编辑时实时解析、blur 时校验。

tsx
import { JsonTextArea } from '@nocobase/client-v2';

<Form.Item name="customConfig" label={t('自定义配置')}>
  <JsonTextArea rows={6} json5 />
</Form.Item>

主要属性:

  • space:序列化缩进,默认 2
  • json5:开启后用 JSON5 解析(容忍尾逗号、注释、单引号等)。默认关
  • showError:解析失败时是否在下方显示错误消息。默认 true
  • 其他 antd Input.TextArea 的属性都透传

value / onChange 的类型是 unknown——因为 JSON 可以是任意结构。调用方按业务约束在 Form.Item.rules 里加 validator 收紧类型。

数据表

Table

设置页表格的标准组件,基于 antd Table 扩展了两点:

  1. 行索引和复选框切换:默认状态显示「1 / 2 / 3」行号,悬停或选中时切换成 checkbox。两个元素绝对定位在同一格内,不会抢空间。需要 rowSelection 才生效
  2. 拖拽排序:传 isDraggable 开启,每行左侧出现拖拽手柄;放下时触发 onSortEnd。组件不动 dataSource,由调用方在回调里跑 resource.move(...)refresh()
tsx
import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';

<Table<AuthenticatorRecord>
  rowKey="id"
  loading={loading}
  columns={columns}
  dataSource={data?.records || []}
  isDraggable
  onSortEnd={async (from, to) => {
    await resource.move({ sourceId: from.id, targetId: to.id });
    refresh();
  }}
  rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
  pagination={{
    current: page,
    pageSize,
    total: data?.total || 0,
    onChange: (next, nextSize) => { /* ... */ },
  }}
/>

主要属性:

  • rowKey:必填。拖拽和行身份识别都依赖它
  • showIndex:默认 true,关掉就只显示 checkbox
  • isDraggable:开关拖拽。默认 false,关掉就是个加强版 antd Table
  • onSortEnd: (from, to) => void | Promise:拖拽放下时触发。调用方负责持久化
  • showSortHandle:默认 true,需要时可以隐藏手柄,自己在某列里嵌 <SortHandle />
  • 其他 antd Table props 全部透传

附带导出:

  • DEFAULT_PAGE_SIZE(值 50):建议的默认分页大小
  • PAGE_SIZE_OPTIONS:建议的分页选项 [5, 10, 20, 50, 100, 200]
  • SortHandle:从 @nocobase/client-v2 导出的独立手柄组件,可以嵌进自定义列

筛选

CollectionFilter

绑定 Collection 的筛选按钮。点击展开 Popover,里面是多条件筛选表单(字段选择器 + 操作符 + 取值控件)。Submit 收起 Popover 并通过 onChange 发出 NocoBase filter 参数;Reset 保持 Popover 打开并发出 undefined

tsx
import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
import lockedUsersCollection from '../../collections/locked-users';

function Page() {
  const main = engine.context.dataSourceManager?.getDataSource?.('main');
  const collection = main?.getCollection?.(lockedUsersCollection.name);

  const listRequest = useRequest(
    async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
    { defaultParams: [undefined] },
  );

  return (
    <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
      <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
    </ExtendCollectionsProvider>
  );
}

主要属性:

  • collection:作为字段来源的 Collection。undefined 时按钮 disabled
  • onChange: (filter) => void:Submit 或 Reset 时触发,参数是编译好的 NocoBase filter(Reset 时为 undefined)。常见做法是直接转给 listRequest.run
  • t:翻译函数。建议传 useT()(来自插件 locale.ts),它会自动展开服务端返回的 {{t("…")}} 模板,否则字段标签、操作符标签可能显示成字面模板
  • filterableFieldNames:白名单,限制顶层可选字段
  • noIgnore:忽略白名单
  • buttonText:覆盖按钮文字,默认 t('Filter')
  • showCount:是否在按钮上显示当前条件数 (N),默认 true
  • popoverProps / buttonProps:透传给 antd Popover / Button
  • popoverMinWidth:Popover 内容最小宽度,默认 520

要筛选的 Collection 如果是 schema-only(服务端没自动发布到客户端 data source),用 <ExtendCollectionsProvider> 包一下当前页面,让 CollectionFilter 能解析到。

工具

createFormRegistry

带命名空间的「条目注册表」工厂。每次调用返回一个独立的 registry 实例,闭包持有自己的 Map

ts
import { createFormRegistry, type FormRegistryEntry } from '@nocobase/client-v2';

interface StorageType extends FormRegistryEntry {
  // FormRegistryEntry 要求至少有 `name: string`
  title: string;
  Component: React.ComponentType;
}

const storageTypes = createFormRegistry<StorageType>('file-manager/storage-types');

storageTypes.register({ name: 'local', title: '本地存储', Component: LocalStorageForm });
storageTypes.register({ name: 's3', title: 'Amazon S3', Component: S3StorageForm });

storageTypes.get('s3');
storageTypes.list();
storageTypes.has('local');
storageTypes.unregister('local');

主要用在:插件需要给「同名 + 同形 + 不同实现」的东西做扩展点(比如 file-manager 的存储类型、verification 的 OTP provider)。比 Map 多了 namespace 标识和 HMR 友好的覆盖警告。

name 重复注册会用新条目覆盖旧的,同时打 console.warn——HMR 时不抛错,开发期能看到意外的重复。

data-source/

跟数据源 / Collection 注册相关的组件。从 @nocobase/client-v2 顶层 export。

ExtendCollectionsProvider

挂载期 Collection 注入器。在组件挂载时把传入的 collection 注册到目标 data source,卸载时移除;会监听 dataSource:loaded 自动重新注入,确保数据源 reload 时不会被清掉。

tsx
import { ExtendCollectionsProvider } from '@nocobase/client-v2';
import lockedUsersCollection from '../../collections/locked-users';

// 模块级常量——保证引用稳定,避免 provider 每次父级重渲染都重跑 effect
const collections = [lockedUsersCollection];

export function LockedUsersPage() {
  return (
    <ExtendCollectionsProvider collections={collections}>
      <LockedUsersPageInner />
    </ExtendCollectionsProvider>
  );
}

主要属性:

  • collections: CollectionOptions[]:本次要注入的 Collection。Provider 只会注册当时不存在的那些,卸载时也只移除自己注册过的
  • dataSource:目标 data source key,默认 'main'
  • children:被注入 Collection 覆盖的子树

什么时候用:

  • 服务端 collection 是 schema-only,不会自动发布到客户端 data source(比如 lockedUsers
  • 需要一个纯客户端的 collection 镜像,只对当前页面有效,不污染全局

常见搭配:跟 <CollectionFilter> 一起用——前者把 collection 挂上,后者读取并渲染筛选表单。

怎么决定加不加新组件

  • 出现两个及以上插件需要同一形态的字段或容器——抽到这里
  • 跨插件复用、但耦合到具体业务领域(比如「选一个 verifier」「选一个数据源」)——留在业务插件里,从插件的 client-v2/ 自己 export
  • 抽象前先看现有组件能不能改进:比如 RemoteSelectselectItems 就是为了让带元数据的响应不需要再开新组件

新增组件后别忘记两件事:

  1. form/index.tsx 加一行 export * from './XxxComponent'
  2. 回来这份 README 补一节,方便后续插件迁移时找到