关于 payload-admin-components
这是一份在 Payload CMS 管理面板中添加自定义 React 组件的参考技能,适合需要定制全局、集合、全局文档及字段界面的开发者。
主要功能
- 01定制管理面板根组件
- 02添加集合和全局组件
- 03构建自定义字段与单元格界面
- 04配置组件路径和属性
- 05使用服务端与客户端组件
- 06访问 Payload API 和翻译
适用场景
- 01替换管理面板标志或导航
- 02在仪表板或页眉添加操作项
- 03创建自定义字段编辑和列表显示
- 04在服务端组件中加载 Payload 数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ timschoch/ skilly --skill payload-admin-components安装后在 Claude Code 中输入/payload-admin-components确认已加载。
详细介绍
Payload CMS 中的自定义组件
自定义组件允许你通过替换为自己的 React 组件,全面自定义管理面板。你几乎可以替换界面的任何部分,或添加全新功能。
组件类型
自定义组件主要有四种类型:
- 根组件 - 影响整个管理面板(徽标、导航、页眉)
- 集合组件 - 用于特定集合视图
- 全局组件 - 用于特定全局文档视图
- 字段组件 - 自定义字段 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
},
},
})
组件路径规则:
- 路径相对于项目根目录(或
config.admin.importMap.baseDir) - 对于命名导出:追加
#ExportName,或使用exportName属性 - 对于默认导出:无需后缀
- 可以省略文件扩展名
组件配置对象
你可以传入配置对象,而不是字符串路径:
{
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 会在将这些属性传递给客户端组件时自动移除。
默认属性
所有自定义组件默认都会收到以下属性:
| 属性 | 描述 | 类型 |
|---|---|---|
payload | Payload 实例(可访问 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"
}
}
组件未加载
- 检查文件路径是否正确(相对于 baseDir)
- 确认命名导出的语法:
/path/to/file#ExportName - 运行
payload generate:importmap重新生成 - 检查组件文件中是否存在 TypeScript 错误