替换为自定义 React 组件

Payload 的 Admin Panel 设计尽可能简洁直观,以便轻松定制并完全控制 UI。为了实现这种级别的定制化,Payload 提供了一种模式,让你可以通过 Payload Config 提供自己的 React 组件。

Payload 中的所有 Custom Components 默认都是 React Server Components。这使得可以直接在前端使用 Local API。几乎 Admin Panel 的每个部分都可以使用 Custom Components,实现极致的细粒度控制。

注意: Client Components 仍然完全支持。要在应用中使用 Client Components,只需包含 'use client' 指令。Payload 会自动检测并在渲染组件前移除所有 不可序列化 的默认 props。更多详情

Payload 中有四种主要的 Custom Components 类型:

要替换为你自己的 Custom Component,首先确定与你目标相符的作用域,查阅可用组件列表,然后构建你的 React 组件

定义自定义组件

当 Payload 编译 Admin Panel 时,它会检查你的配置中是否有自定义组件。当检测到时,Payload 会用自己的默认组件替换为你的组件,或者如果默认没有组件,则直接渲染你的组件。虽然 Payload 中有许多地方支持自定义组件,但每个组件都使用组件路径以相同的方式定义。

要添加自定义组件,请在 Payload 配置中指向其文件路径:

import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  admin: {
    components: {
      logout: {
        Button: '/src/components/Logout#MyComponent', // highlight-line
      },
    },
  },
})

注意: 所有自定义组件都可以是 Server Components 或 Client Components,具体取决于文件顶部是否存在 'use client' 指令。

组件路径

为了确保 Payload 配置完全兼容 Node.js 并尽可能轻量,组件不会直接导入到你的配置中。相反,它们通过文件路径标识,由 Admin Panel 自行解析。

默认情况下,组件路径是相对于项目根目录的。根目录可以是当前工作目录,或者在 config.admin.importMap.baseDir 中指定的目录。

使用命名导出的组件可以通过在路径后追加 # 加上导出名称来标识,或者使用 exportName 属性。如果组件是默认导出,则可以省略这部分。

import { buildConfig } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

const config = buildConfig({
  // ...
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'), // highlight-line
    },
    components: {
      logout: {
        Button: '/components/Logout#MyComponent', // highlight-line
      },
    },
  },
})

在这个例子中,我们将基础目录设置为 src 目录,并在组件路径字符串中省略了 /src/ 部分。

组件配置

虽然自定义组件通常被定义为字符串,但你也可以传入一个包含额外选项的对象:

import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  admin: {
    components: {
      logout: {
        // highlight-start
        Button: {
          path: '/src/components/Logout',
          exportName: 'MyComponent',
        },
        // highlight-end
      },
    },
  },
})

以下是可用的配置选项:

Property描述
clientProps如果组件是客户端组件,传递给自定义组件的属性。更多详情
exportName除了在组件路径中使用 # 声明命名导出外,你也可以省略路径中的命名导出,在这里传入。
path自定义组件的文件路径。命名导出可以通过 # 分隔符附加在路径末尾。
serverProps如果组件是服务端组件,传递给自定义组件的属性。更多详情

关于如何构建自定义组件的详细信息,请参阅构建自定义组件

导入映射表

为了让 Payload 能够使用组件路径,系统会自动在 src/app/(payload)/admin/importMap.jsapp/(payload)/admin/importMap.js 位置生成一个"导入映射表"。该文件包含了你配置中的所有自定义组件,并按各自的路径进行索引。当 Payload 需要查找组件时,会使用此文件来找到正确的导入路径。

导入映射表会在以下情况下自动重新生成:

  • 启动时
  • 热模块替换(HMR)运行时 你也可以运行 payload generate:importmap 命令手动重新生成。

覆盖导入映射表位置

通过 config.admin.importMap.importMapFile 属性,你可以覆盖导入映射表的位置。这在以下情况下很有用:

  • 想要将导入映射表放在不同位置时
  • 想要使用自定义文件名时
import { buildConfig } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

const config = buildConfig({
  // ...
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'),
      importMapFile: path.resolve(
        dirname,
        'app',
        '(payload)',
        'custom-import-map.js',
      ), // highlight-line
    },
  },
})

自定义导入

如有需要,可以将自定义项添加到 Import Map 中。这主要适用于插件开发者,他们需要添加未被引用在已知位置的自定义导入。

要在 Import Map 中添加自定义导入,请使用 Payload Config 中的 admin.dependencies 属性:

import { buildConfig } from 'payload'

export default buildConfig({
  // ...
  admin: {
    // ...
    dependencies: {
      myTestComponent: {
        // myTestComponent 是键名 - 可以是任意名称
        path: '/components/TestComponent.js#TestComponent',
        type: 'component',
        clientProps: {
          test: 'hello',
        },
      },
    },
  },
})

构建自定义组件

Payload 中的所有自定义组件默认都是 React Server Components。这使得可以直接在前端使用 Local API 等功能。

默认属性

为了尽可能简化自定义组件的构建过程,Payload 会自动提供一些常用属性,例如 payload 类和 i18n 对象。这意味着在 Admin Panel 中构建自定义组件时,你无需自行获取这些属性。

以下是一个示例:

import React from 'react'
import type { Payload } from 'payload'

async function MyServerComponent({
  payload, // highlight-line
}: {
  payload: Payload
}) {
  const page = await payload.findByID({
    collection: 'pages',
    id: '123',
  })

  return <p>{page.title}</p>
}

每个自定义组件默认会接收以下属性:

属性描述
payloadPayload 类。
i18ni18n 对象。

提醒: 所有自定义组件还会接收与所渲染组件相关的其他各种属性。完整属性列表请参考 Root ComponentsCollection ComponentsGlobal ComponentsField Components

自定义属性

你也可以向自定义组件传递自定义属性。根据你的属性是否可序列化,以及你的组件是服务端组件还是客户端组件,你可以使用 clientPropsserverProps 属性来实现。

import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  admin: {
    // highlight-line
    components: {
      logout: {
        Button: {
          path: '/src/components/Logout#MyComponent',
          clientProps: {
            myCustomProp: 'Hello, World!', // highlight-line
          },
        },
      },
    },
  },
})

以下是你的组件接收这个属性的方式:

import React from 'react'
import { Link } from '@payloadcms/ui'

export function MyComponent({ myCustomProp }: { myCustomProp: string }) {
  return <Link href="/admin/logout">{myCustomProp}</Link>
}

客户端组件

Payload 中的所有自定义组件默认都是 React Server Components,但你可以通过在文件顶部添加 'use client' 指令来使用 Client Components。Payload 会在渲染组件前自动检测并移除所有 不可序列化 的默认 props。

// highlight-start
'use client'
// highlight-end
import React, { useState } from 'react'

export function MyClientComponent() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>已点击 {count}</button>
  )
}

提醒: 客户端组件不能传递 不可序列化的 props。如果你在 Server Component 中渲染 Client Component,请确保其 props 是可序列化的。

访问 Payload 配置

在任何 Server Component 中,都可以直接从 payload 属性访问 Payload 配置

import React from 'react'

export default async function MyServerComponent({
  payload: {
    config, // highlight-line
  },
}) {
  return <Link href={config.serverURL}>Go Home</Link>
}

但是,Payload 配置在设计上是 不可序列化的。它包含大量自定义验证函数等。这意味着 Payload 配置无法直接完整传递给 Client Components。

为此,Payload 会创建一个 Client Config 并传递给 Config Provider。这是一个可序列化的 Payload 配置版本,可以通过 useConfig 钩子在任意 Client Component 中访问:

'use client'
import React from 'react'
import { useConfig } from '@payloadcms/ui'

export function MyClientComponent() {
  // highlight-start
  const {
    config: { serverURL },
  } = useConfig()
  // highlight-end

  return <Link href={serverURL}>Go Home</Link>
}

更多详情请参阅 使用钩子

同样,所有 字段组件 都会通过 props 自动接收它们各自的字段配置。

在 Server Components 中,这个 prop 名为 field

import React from 'react'
import type { TextFieldServerComponent } from 'payload'

export const MyClientFieldComponent: TextFieldServerComponent = ({
  field: { name },
}) => {
  return <p>{`This field's name is ${name}`}</p>
}

在 Client Components 中,这个 prop 名为 clientField,因为其中不可序列化的 props 已被移除:

'use client'
import React from 'react'
import type { TextFieldClientComponent } from 'payload'

export const MyClientFieldComponent: TextFieldClientComponent = ({
  clientField: { name },
}) => {
  return <p>{`This field's name is ${name}`}</p>
}

获取当前语言

所有自定义组件都可以支持语言翻译,以保持与 Payload 的 I18n 一致。这将使你的自定义组件能够根据用户偏好显示正确的语言。

要实现这一点,首先需要将你的翻译资源添加到 I18n Config。然后,在任何 Server Component 中,你可以使用 @payloadcms/translations 中的 getTranslation 函数来翻译资源。

默认情况下,所有 Server Component 都会自动接收 i18n 对象作为 prop:

import React from 'react'
import { getTranslation } from '@payloadcms/translations'

export default async function MyServerComponent({ i18n }) {
  const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line

  return <p>{translatedTitle}</p>
}

在 Client Component 中实现这一功能的最佳方式是使用 @payloadcms/ui 中的 useTranslation 钩子:

'use client'
import React from 'react'
import { useTranslation } from '@payloadcms/ui'

export function MyClientComponent() {
  const { t, i18n } = useTranslation() // highlight-line

  return (
    <ul>
      <li>{t('namespace1:key', { variable: 'value' })}</li>
      <li>{t('namespace2:key', { variable: 'value' })}</li>
      <li>{i18n.language}</li>
    </ul>
  )
}

查看 Hooks 文档获取可用钩子的完整列表。

获取当前语言环境

所有自定义视图都可以支持多语言环境,以与 Payload 的本地化功能保持一致。这可以用于限定 API 请求范围等。

所有 Server Components 默认会自动接收 locale 对象作为 prop:

import React from 'react'

export default async function MyServerComponent({ payload, locale }) {
  const localizedPage = await payload.findByID({
    collection: 'pages',
    id: '123',
    locale,
  })

  return <p>{localizedPage.title}</p>
}

在 Client Component 中实现这一功能的最佳方式是使用 @payloadcms/ui 提供的 useLocale 钩子:

'use client'
import React from 'react'
import { useLocale } from '@payloadcms/ui'

function Greeting() {
  const locale = useLocale() // highlight-line

  const trans = {
    en: 'Hello',
    es: 'Hola',
  }

  return <span>{trans[locale.code]}</span>
}

完整可用钩子列表请参阅钩子文档

使用钩子

为了更便捷地构建自定义组件,你可以在任何 Client Component 中使用Payload 内置的 React 钩子。例如,你可能需要与 Payload 的某个 React Context 进行交互。为此,你可以根据需求选择使用众多可用钩子中的一个。

'use client'
import React from 'react'
import { useDocumentInfo } from '@payloadcms/ui'

export function MyClientComponent() {
  const { slug } = useDocumentInfo() // highlight-line

  return <p>{`实体 slug: ${slug}`}</p>
}

完整可用钩子列表请参阅钩子文档

添加样式

Payload 提供了一个强大的 CSS 库,可用于为自定义组件添加样式,使其与 Payload 内置样式保持一致。这将确保你的自定义组件能很好地集成到现有设计系统中,并自动适应可能发生的主题变化。

要应用自定义样式,只需将你的 .css.scss 文件导入到自定义组件中:

import './index.scss'

export function MyComponent() {
  return <div className="my-component">My Custom Component</div>
}

例如,要为自定义组件背景添加颜色,可以使用以下 CSS:

.my-component {
  background-color: var(--theme-elevation-500);
}

Payload 还导出了其 SCSS 库以供复用,其中包含 mixins 等。要使用这些功能,只需在你的 .scss 文件中按如下方式导入:

@import '~@payloadcms/ui/scss';

.my-component {
  @include mid-break {
    background-color: var(--theme-elevation-900);
  }
}

注意: 你也可以深入使用 Payload 自身的组件样式,或轻松应用全局 CSS。更多信息请参阅此处