what-digital/payload-inline-visual-editor
GitHub: what-digital/payload-inline-visual-editor
为 Payload CMS 3.x 提供行内可视化编辑能力的插件,让用户在 Live Preview 中直接双击页面元素编辑字段并拖拽重排区块。
Stars: 2 | Forks: 0
# payload-inline-visual-editor
[](https://www.npmjs.com/package/payload-inline-visual-editor)
[](./LICENSE)
为 [Payload CMS](https://payloadcms.com) 3.x 提供的行内可视化编辑功能 —— 在 Live Preview 中浏览你的站点,切换编辑模式,悬停高亮可编辑区域,双击使用 Payload 原生字段 UI 编辑任意字段,并支持直接在页面上添加 / 复制 / 删除 / 拖拽重排 block。此外,还配备专用的全屏编辑器视图、跨文档编辑抽屉,以及带有保存 / 发布 / locale 切换功能的工具栏。

该编辑器构建于核心 Live Preview 之上:标准的保存 / 发布 / 自动保存 / 版本 / locales 流程保持不变,文档数据流经标准的 Live Preview pipeline,并且每个字段类型(Lexical、uploads、relationships、自定义 plugin 字段)都能免费获得原生编辑能力。
## 界面概览
| | |
|---|---|
| **悬停以查找 block。** 每个标记的区域在悬停时都会显示轮廓,标有它的 block 名称,并带有用于编辑 · 在上方 / 下方添加 · 复制 · 删除 · 重排 · 在 layout 中显示的行控件。
 | **双击 block 进行编辑。** 浮层会渲染该 block 自己的子字段 —— 包括 uploads 和富文本 —— 绑定到与侧边栏相同的文档表单。
 | | **或者编辑单个字段。** 双击任何单独标记的字段,仅会出现该字段的编辑器,并锚定在你点击的位置旁边。
 | **完整的表单依然存在。** 切换 layout 面板,可在页面旁查看原生的 Payload blocks UI —— 两者保持同步,并且“在 layout 中显示”可以从一个 block 跳转到它对应的行。
 | ## 包 | 包 | 描述 | |---|---| | [`payload-inline-visual-editor`](https://www.npmjs.com/package/payload-inline-visual-editor) | Payload plugin(管理端) | | [`@payload-inline-visual-editor/core`](https://www.npmjs.com/package/@payload-inline-visual-editor/core) | 与框架无关的站点 SDK(原生 TS,零运行时依赖) | | [`@payload-inline-visual-editor/react`](https://www.npmjs.com/package/@payload-inline-visual-editor/react) | React/Next 适配器:` `, `payloadEditable()` |
## 环境要求
- `payload@^3.85`(以及 `@payloadcms/ui@^3.85`)
- React 19(Payload 3.x 使用的版本)
- 为你要编辑的 collections/globals 配置可用的 [Live Preview](https://payloadcms.com/docs/live-preview/overview)
## 安装
```
pnpm add payload-inline-visual-editor @payload-inline-visual-editor/react
```
### 1. 添加 plugin
```
// payload.config.ts
import { visualEditor } from 'payload-inline-visual-editor'
export default buildConfig({
// ...
plugins: [visualEditor()],
})
```
默认情况下,编辑器会为每个配置了 Live Preview URL(`admin.livePreview.url`,或者在根目录的 `admin.livePreview` 条目中列出了 slug)的 collection 和 global 自动启用。你也可以显式启用实体 —— 参见 [Plugin 选项](#plugin-options)。
然后重新生成你的 import map,以便解析管理 controller 组件:
```
payload generate:importmap
```
### 2. 标记你的前端
编辑器通过显式的 `data-pve-*` 属性将 DOM 映射到文档数据。路径是相对于文档根节点的表单状态数据路径,例如 `layout.0.heading`。
```
import { payloadEditable } from '@payload-inline-visual-editor/react'
```
- `payloadEditable.scope(...)` 用于标记子树所属的文档 —— 将其放在渲染该文档的最外层元素上。Globals 使用:`payloadEditable.scope({ global: 'header' })`。
- `payloadEditable(path)` 标记一个可编辑的叶子字段。
- `payloadEditable(path, { row })` 标记一个 array/blocks 的行容器。**传入 Payload 在每一行存储的行 `id`** —— block 操作依赖于此。`id`/`blockType` 接受 Payload 生成的可空类型,因此 `row: { id: block.id, blockType: block.blockType }` 可以直接使用。如果在运行时缺少 id,该行会降级为普通字段,并在开发模式下触发警告。
- `payloadEditable.container(path)` 标记一个 array/blocks 容器 —— 它界定了拖放区域,并托管空状态时的“添加”交互入口。
这些辅助函数返回普通的 `Record` 属性,因此它们对 RSC/SSR 是安全的 —— 标记不需要客户端组件。
### 3. 挂载 SDK
```
import { VisualEditing } from '@payload-inline-visual-editor/react'
// In the page (or layout) that Live Preview loads:
```
`serverURL` 是你的 Payload 服务器源地址 —— SDK 只接受来自该源的消息。该组件不会渲染任何内容,仅在 iframe 内部激活,并且在收到来自管理面板的有效握手之前,始终保持完全惰性。
### 4. 编辑
在管理面板中打开一个文档,进入 Live Preview,然后点击 **Edit on page**。悬停会高亮可编辑区域;单击选择一个区域,双击则会展开、滚动并聚焦到管理面板中相应的表单字段。行会显示一个带有编辑 / 添加 / 复制 / 删除 / 在 layout 中显示 / 拖拽重排功能的控件。
## 选择预览模式
两种原生的 Live Preview 模式均可正常工作;它们的区别在于 block 操作反映在页面上的速度:
| 模式 | 设置 | 操作后的行为 |
|---|---|---|
| **Client mode**(推荐用于编辑) | [`useLivePreview`](https://payloadcms.com/docs/live-preview/client) | 新数据立即流式传输到页面(约 100 ms 重新渲染) |
| **SSR mode** | [`RefreshRouteOnSave`](https://payloadcms.com/docs/live-preview/server) | 更改在自动保存 + `router.refresh()` 后生效(1–3 秒);在页面更新完成前,编辑器会在受影响的容器上显示待处理微光效果。此模式下的 Block 操作需要开启 drafts + 自动保存 |
[dev playground](./dev) 演示了这两种模式:[`dev/app/(frontend)/client-mode`](./dev/app/(frontend)/client-mode/%5Bslug%5D/page.client.tsx) 和 [`dev/app/(frontend)/ssr-mode`](./dev/app/(frontend)/ssr-mode/%5Bslug%5D/page.tsx)。
## 专用编辑器视图
除了文档编辑视图中的“Edit on page”开关外,plugin 还注册了一个全屏编辑器,路径为 `/admin/visual-editor/:collectionSlug/:id`(globals 为:`/admin/visual-editor/globals/:globalSlug`)—— 这是一个没有管理导航的纯粹视图:顶部有工具栏(文档状态、保存 / 发布、locale 切换器、退出),其余部分由实时预览填满,并且编辑模式已开启。原生预览工具栏的断点 / 大小 / 缩放控件位于预览上方。字段面板默认隐藏 —— 工具栏中的 **Show layout**(或者 block 控件上的 **show in layout** 按钮,它会滚动到该 block 并折叠其他 block)可以将其打开。
每个启用的文档在其编辑视图控件中都有一个 **Open visual editor** 按钮;工具栏中的 **Exit** 可返回。深链接有效 —— 该视图本身会强制进行管理身份验证。
在该视图中,双击标记的区域 —— 或点击 block 控件上的 **edit** 按钮 —— 会打开一个**近光标浮窗**,仅编辑该字段:叶子区域编辑单个字段,block/array 行编辑该行的字段 —— 绑定到与 layout 面板相同的表单,因此编辑会直接流式传输到预览中,并且保存/发布/自动保存的行为与编辑视图中完全一致。富文本会渲染完整的 Lexical 编辑器;upload 和 relationship 字段保留其原生的抽屉交互流程。可以通过 ✕、Escape 或点击预览中的页面空白区域来关闭它。
双击属于*不同*文档(如 global header、关联文档)的区域,会在不离开当前页面的情况下,在一个抽屉中打开该文档 —— 保存它会流经标准的文档事件路径,因此 SSR 模式下的预览会自动刷新(client 模式仅重新渲染当前文档的数据;其他文档将在下次页面加载时更新)。
该视图及其工具栏是可配置的(参见下文的 plugin 选项):`view: { enabled: false }` 会移除该路由,`view.path` 会重新指定其位置,而 `toolbar` 可以隐藏各个控件。
## 草稿模式与生产环境剔除
**将 Live Preview 指向草稿内容路由。** 编辑操作发生在工作草稿上,因此预览的页面必须渲染草稿内容 —— 标准的 Payload + Next 模式是由 [`draftMode()`](https://payloadcms.com/docs/live-preview/server) 和预览密钥控制的预览路由(参见官方的 `templates/website`)。通过 `draft` 属性告知 SDK 它正在查看的内容 —— 当预览仅渲染已发布内容时,管理端会记录一条警告(编辑内容在发布前不会显示):
```
```
**从生产环境 HTML 中剔除属性。** 如果 `data-pve-*` 属性发生泄漏也是无害的(在没有经过身份验证的管理端握手时,SDK 是惰性的),但你可以保持公共 HTML 的整洁:
```
import { configurePayloadEditable } from '@payload-inline-visual-editor/react'
// Next: at the top of the preview-capable page/layout render
import { draftMode } from 'next/headers'
configurePayloadEditable({ enabled: (await draftMode()).isEnabled })
```
当 `enabled` 为 `false` 时,每个辅助函数都会返回 `{}` —— 渲染的 HTML 中不会包含任何属性。如果你的站点构建从不服务于编辑器(例如独立的静态生产构建),使用静态门控也可以:
```
configurePayloadEditable({ enabled: process.env.NODE_ENV !== 'production' })
```
## 跨域设置
文档记录的默认路径是一个单一的 Next 应用,同时提供站点和 `/admin` 服务(同源 —— 无需配置)。如果你的站点和 Payload 服务器运行在**不同的源**上,预览的站点(在 iframe 内)必须能够使用管理端的身份验证 cookie 调用 Payload REST API:
```
// payload.config.ts
export default buildConfig({
cors: ['https://site.example.com'],
csrf: ['https://site.example.com'],
// The admin cookie must be sendable from the iframe's third-party context:
// on your auth-enabled collection —
// auth: { cookies: { sameSite: 'None', secure: true } }
})
```
- Client 模式的 live-preview 填充请求以及来自站点的任何草稿内容抓取,必须使用 `credentials: 'include'`。
- `sameSite: 'None'` 需要 `secure: true`(两个源都需要 HTTPS)。
- 确保站点允许被管理端的源嵌入(预览路由上没有一刀切的 `X-Frame-Options: DENY` 或严格的 `frame-ancestors`)。
visual editor 自身的管理端↔站点通道是 `postMessage`,具有严格的源 + 会话检查,不需要 CORS。
## Plugin 选项
```
visualEditor({
// Default: every collection/global with a Live Preview URL.
// Listing entities explicitly enables only those listed:
collections: {
pages: true,
posts: {
url: ({ data }) => `${serverURL}/posts/${data?.slug}`, // override admin.livePreview.url
blockOps: { add: true, duplicate: true, remove: false, move: true }, // or false to disable all
},
},
globals: { header: true },
view: {
enabled: true, // false removes the dedicated view route
path: '/visual-editor/:collectionSlug/:id', // must keep both params; globals mount at …/globals/:globalSlug
},
toolbar: {
locale: true, // locale switcher in the dedicated view (localized projects)
breakpoint: true, // stock preview toolbar (breakpoint / size / zoom) above the preview
exit: true, // "Exit" link back to the standard edit view
},
debug: false, // verbose console logging on both sides of the bridge
})
```
一个没有配置 Live Preview URL(且没有 `url` 覆盖)的显式列出的实体,会在启动时抛出带有可操作信息的异常。
基于实体的 `blockOps` 控制覆盖层提供哪些行操作。字段级别的权限会在此基础上应用:覆盖层会隐藏当前用户无权更新的路径对应的交互入口,并且 Payload 的服务端访问控制始终保持权威性。
## 属性参考
对于非 React 前端,`@payload-inline-visual-editor/core` 以原生函数的形式提供了相同的辅助方法(`pveAttr.scope/field/row/container`),或者你也可以手动编写这些属性:
| 属性 | 含义 | 示例 |
|---|---|---|
| `data-pve-scope` | 文档范围;最近的祖先节点生效 | `collection:pages;id:665f…`, `global:header` |
| `data-pve-field` | 可编辑的叶子字段(数据路径) | `layout.0.heading` |
| `data-pve-field` + `data-pve-row` (+ `data-pve-block-type`) | Array/blocks 行容器 | `layout.0` + row id + `hero` |
| `data-pve-container` | Array/blocks 容器 | `layout` |
不使用 React 进行挂载:
```
import { initVisualEditing } from '@payload-inline-visual-editor/core'
if (window.self !== window.top) {
const teardown = initVisualEditing({ serverURL: 'https://cms.example.com' })
}
```
## 安全模型
- 在收到来自已配置管理端的合法 `hello` 消息之前,SDK 是惰性的;每条消息都会根据源、`event.source` 和每次加载的 session id 进行检查。没有 `'*'` postMessage 目标。
- 意图通道仅传输路径和行 id —— 文档数据完全流经 Payload 原生的 Live Preview pipeline。
- 编辑模式只能从经过身份验证的管理会话中发起;所有修改都在管理表单中执行,并在保存时通过 Payload 的访问控制。
## 开发
```
pnpm install
pnpm build # build packages (core → plugin/react)
pnpm test # vitest int tests (run build first)
pnpm dev # playground at http://localhost:3000
pnpm e2e # Playwright e2e (starts the playground itself)
```
Playground 管理端:`http://localhost:3000/admin` —— `dev@payloadcms.com` / `test`(首次运行时;`viewer@payloadcms.com` / `test` 是用于权限测试的只读账户)。打开植入的 **Home** 页面并切换到 Live Preview。
## 更新日志
发布说明请参见 [CHANGELOG.md](./CHANGELOG.md)。
## 许可证
[MIT](./LICENSE)
 | **双击 block 进行编辑。** 浮层会渲染该 block 自己的子字段 —— 包括 uploads 和富文本 —— 绑定到与侧边栏相同的文档表单。
 | | **或者编辑单个字段。** 双击任何单独标记的字段,仅会出现该字段的编辑器,并锚定在你点击的位置旁边。
 | **完整的表单依然存在。** 切换 layout 面板,可在页面旁查看原生的 Payload blocks UI —— 两者保持同步,并且“在 layout 中显示”可以从一个 block 跳转到它对应的行。
 | ## 包 | 包 | 描述 | |---|---| | [`payload-inline-visual-editor`](https://www.npmjs.com/package/payload-inline-visual-editor) | Payload plugin(管理端) | | [`@payload-inline-visual-editor/core`](https://www.npmjs.com/package/@payload-inline-visual-editor/core) | 与框架无关的站点 SDK(原生 TS,零运行时依赖) | | [`@payload-inline-visual-editor/react`](https://www.npmjs.com/package/@payload-inline-visual-editor/react) | React/Next 适配器:`
{page.layout?.map((block, i) => (
))}
{block.heading}
标签:Payload CMS, React, Syscall, Syscalls, Web开发, 前端组件, 可视化编辑, 所见即所得, 自动化攻击