mcpField
P
payload-admin-components

payload-admin-components

github.com/timschoch开发161 次安装今天更新
代码仓库

关于 payload-admin-components

这是一份在 Payload CMS 管理面板中添加自定义 React 组件的参考技能,适合需要定制全局、集合、全局文档及字段界面的开发者。

主要功能

  1. 01定制管理面板根组件
  2. 02添加集合和全局组件
  3. 03构建自定义字段与单元格界面
  4. 04配置组件路径和属性
  5. 05使用服务端与客户端组件
  6. 06访问 Payload API 和翻译

适用场景

  1. 01替换管理面板标志或导航
  2. 02在仪表板或页眉添加操作项
  3. 03创建自定义字段编辑和列表显示
  4. 04在服务端组件中加载 Payload 数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/timschoch/skilly --skill payload-admin-components

安装后在 Claude Code 中输入/payload-admin-components确认已加载。

详细介绍

Payload CMS 中的自定义组件

自定义组件允许你通过替换为自己的 React 组件,全面自定义管理面板。你几乎可以替换界面的任何部分,或添加全新功能。

组件类型

自定义组件主要有四种类型:

  1. 根组件 - 影响整个管理面板(徽标、导航、页眉)
  2. 集合组件 - 用于特定集合视图
  3. 全局组件 - 用于特定全局文档视图
  4. 字段组件 - 自定义字段 UI 和单元格

定义自定义组件

组件路径

组件使用文件路径定义(而非直接导入),以保持配置轻量并兼容 Node.js。

import { buildConfig } from 'payload'

export default buildConfig({
  admin: {
    components: {
      logout: {
        Button: '/src/components/Logout#MyComponent', // Named export
      },
      Nav: '/src/components/Nav', // Default export
    },
  },
})

组件路径规则:

  1. 路径相对于项目根目录(或 config.admin.importMap.baseDir)
  2. 对于命名导出:追加 #ExportName,或使用 exportName 属性
  3. 对于默认导出:无需后缀
  4. 可以省略文件扩展名
组件配置对象

你可以传入配置对象,而不是字符串路径:

{
  logout: {
    Button: {
      path: '/src/components/Logout',
      exportName: 'MyComponent',
      clientProps: { customProp: 'value' },
      serverProps: { asyncData: someData },
    },
  },
}

配置属性:

属性描述
path组件的文件路径(命名导出通过 # 指定)
exportName命名导出(作为路径中 # 的替代方式)
clientProps客户端组件的属性(必须可序列化)
serverProps服务端组件的属性(可以不可序列化)
设置基础目录
import path from 'path'
import { fileURLToPath } from 'node:url'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export default buildConfig({
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'), // Set base directory
    },
    components: {
      Nav: '/components/Nav', // Now relative to src/
    },
  },
})

服务端组件与客户端组件

所有组件默认都是 React 服务端组件。

服务端组件(默认)

可以直接使用 Local API、执行异步操作,并访问完整的 Payload 实例。

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

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

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

export default MyServerComponent
客户端组件

使用 'use client' 指令来实现交互、使用钩子、管理状态等。

'use client'
import React, { useState } from 'react'

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

  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
}

**重要:**客户端组件不能接收不可序列化的属性(函数、类实例等)。Payload 会在将这些属性传递给客户端组件时自动移除。

默认属性

所有自定义组件默认都会收到以下属性:

属性描述类型
payloadPayload 实例(可访问 Local API)Payload
i18n国际化对象I18n
locale当前区域设置(启用本地化时)string

服务端组件示例:

async function MyComponent({ payload, i18n, locale }) {
  const data = await payload.find({
    collection: 'posts',
    locale,
  })

  return <div>{data.docs.length} posts</div>
}

客户端组件示例:

'use client'
import { usePayload, useLocale, useTranslation } from '@payloadcms/ui'

export function MyComponent() {
  // Access via hooks in client components
  const { getLocal, getByID } = usePayload()
  const locale = useLocale()
  const { t, i18n } = useTranslation()

  return <div>{t('myKey')}</div>
}

自定义属性

使用 clientProps 或 serverProps 传递其他属性:

{
  logout: {
    Button: {
      path: '/components/Logout',
      clientProps: {
        buttonText: 'Sign Out',
        onLogout: () => console.log('Logged out'),
      },
    },
  },
}

在组件中接收:

'use client'
export function Logout({ buttonText, onLogout }) {
  return <button onClick={onLogout}>{buttonText}</button>
}

根组件

根组件会影响整个管理面板。

可用的根组件
组件描述配置路径
Nav整个导航侧边栏admin.components.Nav
graphics.Icon小图标(用于导航)admin.components.graphics.Icon
graphics.Logo完整徽标(用于登录页面)admin.components.graphics.Logo
logout.Button注销按钮admin.components.logout.Button
actions页眉操作(数组)admin.components.actions
header页眉上方(数组)admin.components.header
beforeDashboard仪表板内容之前(数组)admin.components.beforeDashboard
afterDashboard仪表板内容之后(数组)admin.components.afterDashboard
beforeLogin登录表单之前(数组)admin.components.beforeLogin
afterLogin登录表单之后(数组)admin.components.afterLogin
beforeNavLinks导航链接之前(数组)admin.components.beforeNavLinks
afterNavLinks导航链接之后(数组)admin.components.afterNavLinks
settingsMenu设置菜单项(数组)admin.components.settingsMenu
providers自定义 React Context 提供程序admin.components.providers
views自定义视图(仪表板等)admin.components.views
示例:自定义徽标
export default buildConfig({
  admin: {
    components: {
      graphics: {
        Logo: '/components/Logo',
        Icon: '/components/Icon',
      },
    },
  },
})
// components/Logo.tsx
export default function Logo() {
  return <img src="/logo.png" alt="My Brand" width={200} />
}
示例:页眉操作
export default buildConfig({
  admin: {
    components: {
      actions: ['/components/ClearCacheButton', '/components/PreviewButton'],
    },
  },
})
// components/ClearCacheButton.tsx
'use client'
export default function ClearCacheButton() {
  return (
    <button
      onClick={async () => {
        await fetch('/api/clear-cache', { method: 'POST' })
        alert('Cache cleared!')
      }}
    >
      Clear Cache
    </button>
  )
}

集合组件

集合组件专用于集合的视图。

import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    components: {
      // Edit view components
      edit: {
        PreviewButton: '/components/PostPreview',
        SaveButton: '/components/CustomSave',
        SaveDraftButton: '/components/CustomSaveDraft',
        PublishButton: '/components/CustomPublish',
      },

      // List view components
      list: {
        Header: '/components/PostsListHeader',
        beforeList: ['/components/ListFilters'],
        afterList: ['/components/ListFooter'],
      },
    },
  },
  fields: [
    // ...
  ],
}

全局组件

与集合组件类似,但用于全局文档。

import type { GlobalConfig } from 'payload'

export const Settings: GlobalConfig = {
  slug: 'settings',
  admin: {
    components: {
      edit: {
        PreviewButton: '/components/SettingsPreview',
        SaveButton: '/components/SettingsSave',
      },
    },
  },
  fields: [
    // ...
  ],
}

字段组件

自定义字段在编辑和列表视图中的呈现方式。

字段组件(编辑视图)
{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    components: {
      Field: '/components/StatusField',
    },
  },
}
// components/StatusField.tsx
'use client'
import { useField } from '@payloadcms/ui'
import type { SelectFieldClientComponent } from 'payload'

export const StatusField: SelectFieldClientComponent = ({ path, field }) => {
  const { value, setValue } = useField({ path })

  return (
    <div>
      <label>{field.label}</label>
      <select value={value} onChange={(e) => setValue(e.target.value)}>
        {field.options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
    </div>
  )
}
单元格组件(列表视图)
{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    components: {
      Cell: '/components/StatusCell',
    },
  },
}
// components/StatusCell.tsx
import type { SelectFieldCellComponent } from 'payload'

export const StatusCell: SelectFieldCellComponent = ({ data, cellData }) => {
  const isPublished = cellData === 'published'

  return (
    <span
      style={{
        color: isPublished ? 'green' : 'orange',
        fontWeight: 'bold',
      }}
    >
      {cellData}
    </span>
  )
}
UI 字段(仅用于呈现)

用于添加自定义 UI 而不影响数据的特殊字段类型:

{
  name: 'refundButton',
  type: 'ui',
  admin: {
    components: {
      Field: '/components/RefundButton',
    },
  },
}
// components/RefundButton.tsx
'use client'
import { useDocumentInfo } from '@payloadcms/ui'

export default function RefundButton() {
  const { id } = useDocumentInfo()

  return (
    <button
      onClick={async () => {
        await fetch(`/api/orders/${id}/refund`, { method: 'POST' })
        alert('Refund processed')
      }}
    >
      Process Refund
    </button>
  )
}

使用 Hooks

Payload 为客户端组件提供了许多 React Hooks:

'use client'
import {
  useAuth, // Current user
  useConfig, // Payload config (client-safe)
  useDocumentInfo, // Current document info (id, slug, etc.)
  useField, // Field value and setValue
  useForm, // Form state and dispatch
  useFormFields, // Multiple field values (optimized)
  useLocale, // Current locale
  useTranslation, // i18n translations
  usePayload, // Local API methods
} from '@payloadcms/ui'

export function MyComponent() {
  const { user } = useAuth()
  const { config } = useConfig()
  const { id, collection } = useDocumentInfo()
  const locale = useLocale()
  const { t } = useTranslation()

  return <div>Hello {user?.email}</div>
}

**重要:**这些 Hooks 仅在管理面板上下文中的客户端组件内有效。

访问 Payload 配置

在服务端组件中:

async function MyServerComponent({ payload }) {
  const { config } = payload
  return <div>{config.serverURL}</div>
}

在客户端组件中:

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

export function MyClientComponent() {
  const { config } = useConfig() // Client-safe config
  return <div>{config.serverURL}</div>
}

**重要:**客户端组件接收的是可序列化版本的配置(函数、验证等内容会被剥离)。

字段配置访问

服务端组件:

import type { TextFieldServerComponent } from 'payload'

export const MyFieldComponent: TextFieldServerComponent = ({ field }) => {
  return <div>Field name: {field.name}</div>
}

客户端组件:

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

export const MyFieldComponent: TextFieldClientComponent = ({ clientField }) => {
  // clientField has non-serializable props removed
  return <div>Field name: {clientField.name}</div>
}

翻译(i18n)

服务端组件:

import { getTranslation } from '@payloadcms/translations'

async function MyServerComponent({ i18n }) {
  const translatedTitle = getTranslation(myTranslation, i18n)
  return <p>{translatedTitle}</p>
}

客户端组件:

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

export function MyClientComponent() {
  const { t, i18n } = useTranslation()

  return (
    <div>
      <p>{t('namespace:key', { variable: 'value' })}</p>
      <p>Language: {i18n.language}</p>
    </div>
  )
}

为组件设置样式

使用 CSS 变量
import './styles.scss'

export function MyComponent() {
  return <div className="my-component">Custom Component</div>
}
// styles.scss
.my-component {
  background-color: var(--theme-elevation-500);
  color: var(--theme-text);
  padding: var(--base);
  border-radius: var(--border-radius-m);
}
导入 Payload SCSS
@import '~@payloadcms/ui/scss';

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

常见模式

条件性字段可见性
'use client'
import { useFormFields } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload'

export const ConditionalField: TextFieldClientComponent = ({ path }) => {
  const showField = useFormFields(([fields]) => fields.enableFeature?.value)

  if (!showField) return null

  return <input type="text" />
}
从 API 加载数据
'use client'
import { useState, useEffect } from 'react'

export function DataLoader() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/custom-data')
      .then((res) => res.json())
      .then(setData)
  }, [])

  return <div>{JSON.stringify(data)}</div>
}
在服务端组件中使用本地 API
import type { Payload } from 'payload'

async function RelatedPosts({ payload, id }: { payload: Payload; id: string }) {
  const post = await payload.findByID({
    collection: 'posts',
    id,
    depth: 0,
  })

  const related = await payload.find({
    collection: 'posts',
    where: {
      category: { equals: post.category },
      id: { not_equals: id },
    },
    limit: 5,
  })

  return (
    <div>
      <h3>Related Posts</h3>
      <ul>
        {related.docs.map((doc) => (
          <li key={doc.id}>{doc.title}</li>
        ))}
      </ul>
    </div>
  )
}

export default RelatedPosts

性能最佳实践

1. 尽量减小客户端包体积
// In Admin Panel UI: import from @payloadcms/ui
'use client'
import { Button } from '@payloadcms/ui'

// In frontend code: use specific paths for tree-shaking
import { Button } from '@payloadcms/ui/elements/Button'
2. 优化重新渲染
// Bad: Re-renders on every form change
'use client'
import { useForm } from '@payloadcms/ui'

export function MyComponent() {
  const { fields } = useForm()
  // Re-renders on ANY field change
}

// Good: Only re-renders when specific field changes
;('use client')
import { useFormFields } from '@payloadcms/ui'

export function MyComponent({ path }) {
  const value = useFormFields(([fields]) => fields[path])
  // Only re-renders when this field changes
}
3. 尽可能使用服务端组件

仅在需要以下功能时使用客户端组件:

  • 状态(useState、useReducer)
  • 副作用(useEffect)
  • 事件处理程序(onClick、onChange)
  • 浏览器 API(localStorage、window)
4. React 最佳实践
  • 对开销较大的组件使用 React.memo()
  • 为列表设置合适的 key 属性
  • 避免在渲染过程中内联定义函数
  • 为异步操作使用 Suspense 边界

导入映射

Payload 会在 app/(payload)/admin/importMap.js 生成导入映射,用于解析所有组件路径。

手动重新生成:

payload generate:importmap

覆盖位置:

export default buildConfig({
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'),
      importMapFile: path.resolve(dirname, 'app', 'custom-import-map.js'),
    },
  },
})

类型安全

为组件使用 Payload 的 TypeScript 类型:

import type {
  TextFieldServerComponent,
  TextFieldClientComponent,
  TextFieldCellComponent,
} from 'payload'

export const MyFieldComponent: TextFieldServerComponent = (props) => {
  // Fully typed props
}

故障排除

“useConfig is undefined” 或类似的钩子错误

原因: Payload 软件包之间的依赖版本不匹配。

解决方案: 将所有 @payloadcms/* 软件包固定为完全相同的版本:

{
  "dependencies": {
    "payload": "3.0.0",
    "@payloadcms/ui": "3.0.0",
    "@payloadcms/richtext-lexical": "3.0.0"
  }
}
组件未加载
  1. 检查文件路径是否正确(相对于 baseDir)
  2. 确认命名导出的语法:/path/to/file#ExportName
  3. 运行 payload generate:importmap 重新生成
  4. 检查组件文件中是否存在 TypeScript 错误

资源

评论

支持 Markdown