Skip to content

插件

插件系统允许你在不修改源码的前提下扩展编辑器:添加自定义区块、分类、工具栏动作、侧边栏面板和属性编辑器。

创建插件

插件是一个接收 PluginContext 的函数:

ts
import type { Plugin } from "@fpfe-group/email-editor-ai";

const myPlugin: Plugin = (ctx) => {
  // 使用 ctx 注册扩展
};

注册插件

vue
<EmailEditor :plugins="[myPlugin]" />

插件会在 editor:ready 事件触发之前初始化。

添加自定义区块

ts
import type { Plugin } from "@fpfe-group/email-editor-ai";
import {
  createText,
  createImage,
  createSection,
  createColumn,
} from "@fpfe-group/email-editor-ai";

const brandPlugin: Plugin = (ctx) => {
  ctx.registerBlock({
    id: "brand-signature",
    label: "Signature",
    category: "content",
    icon: "PenTool",
    factory: () =>
      createText(`
      <p style="font-size: 14px; color: #666;">
        <strong>John Doe</strong><br/>
        Product Manager at Acme Corp<br/>
        <a href="mailto:john@acme.com">john@acme.com</a>
      </p>
    `),
  });

  ctx.registerBlock({
    id: "brand-header",
    label: "Brand Header",
    category: "composite",
    icon: "Layout",
    factory: () => {
      const logo = createImage({
        src: "https://acme.com/logo.png",
        alt: "Acme Corp",
        width: "150px",
      });
      const col = createColumn([logo]);
      return createSection([col], {
        "background-color": "#1a1a2e",
        padding: "20px 0",
      });
    },
  });
};

添加区块分类

ts
const myPlugin: Plugin = (ctx) => {
  ctx.registerBlockCategory({
    id: "brand",
    label: "Brand",
    icon: "Building",
    order: 5, // 数字越小越靠前
  });

  ctx.registerBlock({
    id: "brand-banner",
    label: "Brand Banner",
    category: "brand",
    icon: "Flag",
    factory: () => createText("<h1>Welcome to Acme</h1>"),
  });
};

添加工具栏动作

ts
const savePlugin: Plugin = (ctx) => {
  ctx.registerToolbarAction({
    id: "save",
    label: "Save",
    icon: "Save",
    position: "right",
    order: 10,
    handler: () => {
      console.log("Saving...");
    },
  });
};

监听事件

插件可以访问完整事件系统:

ts
const analyticsPlugin: Plugin = (ctx) => {
  ctx.on("editor:change", ({ document }) => {
    analytics.track("email_edited", {
      nodeCount: countNodes(document.body),
    });
  });

  ctx.on("node:deleted", ({ nodeId }) => {
    console.log(`Node ${nodeId} deleted`);
  });

  ctx.on("block:dropped", ({ blockId, parentId }) => {
    console.log(`Block ${blockId} dropped into ${parentId}`);
  });
};

异步插件

插件可以是异步函数,例如从远端加载自定义区块定义:

ts
const remoteBlocksPlugin: Plugin = async (ctx) => {
  const response = await fetch("/api/custom-blocks");
  const blocks = await response.json();

  for (const block of blocks) {
    ctx.registerBlock({
      id: block.id,
      label: block.name,
      category: "custom",
      icon: block.icon || "Box",
      factory: () => createText(block.defaultHtml),
    });
  }
};

插件上下文 API

方法说明
registerBlock(block)添加区块到「区块」面板
registerBlockCategory(category)添加新的分类标签
registerPropertyEditor(type, component)覆盖某类属性编辑器
registerToolbarAction(action)添加工具栏按钮
registerSidebarPanel(panel)添加侧边栏标签页
on(event, handler)监听编辑器事件
off(event, handler)移除事件监听
labels当前 EditorLabels 的响应式引用

多插件

vue
<EmailEditor :plugins="[brandPlugin, analyticsPlugin, savePlugin]" />

插件按数组顺序执行。重复的 block/category ID 会被静默忽略,第一次注册优先。

基于 MIT License 发布,©FPFE — 保留所有权利。
开源不易,且行且珍惜。