原生插件可以使用自定义 React 页面和仪表板小部件扩展管理面板 — 沙箱插件则将其 UI 描述为 Block Kit,因为将插件 JavaScript 发送到管理界面会破坏沙箱隔离。
如果您的插件只需要一个设置表单,自动生成的 admin.settingsSchema 表单(参见您的第一个原生插件)无需编写任何 React 即可覆盖大多数情况。当您需要比 settingsSchema 提供的更丰富的 UI 时,请使用自定义组件。
管理入口点
具有管理 UI 的插件从 admin 入口点导出 pages 和 widgets 对象:
import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";
export const widgets = {
"seo-overview": SEODashboardWidget,
};
export const pages = {
"/settings": SEOSettingsPage,
};
在 package.json 中配置入口点:
{
"exports": {
".": "./dist/index.js",
"./admin": "./dist/admin.js"
}
}
从 definePlugin() 引用它:
definePlugin({
id: "seo",
version: "1.0.0",
admin: {
entry: "@my-org/plugin-seo/admin",
pages: [{ path: "/settings", label: "SEO Settings", icon: "settings" }],
widgets: [{ id: "seo-overview", title: "SEO Overview", size: "half" }],
},
});
描述符需要一个匹配的 adminEntry,以便 EmDash 知道在构建时在哪里找到组件:
adminEntry: "@my-org/plugin-seo/admin",
管理页面
管理页面是挂载在 /_emdash/admin/plugins/<plugin-id>/<path> 下的 React 组件。
页面定义
admin: {
pages: [
{
path: "/settings",
label: "Settings",
icon: "settings",
},
{
path: "/reports",
label: "Reports",
icon: "chart",
},
],
}
页面组件
import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";
export function SettingsPage() {
const api = usePluginAPI();
const [settings, setSettings] = useState<Record<string, unknown>>({});
const [saving, setSaving] = useState(false);
useEffect(() => {
api.get("settings").then(setSettings);
}, []);
const handleSave = async () => {
setSaving(true);
await api.post("settings/save", settings);
setSaving(false);
};
return (
<div>
<h1>Plugin Settings</h1>
<label>
Site Title
<input
type="text"
value={(settings.siteTitle as string) || ""}
onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
/>
</label>
<button onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save Settings"}
</button>
</div>
);
}
插件 API 钩子
usePluginAPI() 调用您的插件路由,自动添加插件 ID 前缀和 X-EmDash-Request: 1 CSRF 标头:
import { usePluginAPI } from "@emdash-cms/admin";
function MyComponent() {
const api = usePluginAPI();
const data = await api.get("status"); // GET /_emdash/api/plugins/<id>/status
await api.post("settings/save", { enabled: true }); // 带 JSON 正文的 POST
const result = await api.get("history?limit=50"); // 支持查询参数
}
仪表板小部件
小部件出现在管理仪表板上,提供一目了然的信息。
小部件定义
admin: {
widgets: [
{
id: "seo-overview",
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
],
}
小部件组件
import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";
export function SEOWidget() {
const api = usePluginAPI();
const [data, setData] = useState({ score: 0, issues: [] });
useEffect(() => {
api.get("analyze").then(setData);
}, []);
return (
<div className="widget-content">
<div className="score">{data.score}%</div>
<ul>
{data.issues.map((issue, i) => (
<li key={i}>{(issue as { message: string }).message}</li>
))}
</ul>
</div>
);
}
小部件大小
| Size | Description |
|---|---|
full | 仪表板全宽 |
half | 仪表板半宽 |
third | 仪表板三分之一宽度 |
小部件根据屏幕宽度自动换行。
导出结构
管理入口点导出两个对象:
import { SettingsPage } from "./components/SettingsPage";
import { ReportsPage } from "./components/ReportsPage";
import { StatusWidget } from "./components/StatusWidget";
import { OverviewWidget } from "./components/OverviewWidget";
export const pages = {
"/settings": SettingsPage,
"/reports": ReportsPage,
};
export const widgets = {
status: StatusWidget,
overview: OverviewWidget,
};
使用管理组件
EmDash 为常见模式提供预构建的组件:
import {
Card,
Button,
Input,
Select,
Toggle,
Table,
Pagination,
Alert,
Loading,
} from "@emdash-cms/admin";
function SettingsPage() {
return (
<Card title="Settings">
<Input label="API Key" type="password" />
<Toggle label="Enabled" defaultChecked />
<Button variant="primary">Save</Button>
</Card>
);
}
自动生成的设置 UI
如果您的插件只需要一个设置表单,请使用 admin.settingsSchema 而无需自定义组件:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
},
},
EmDash 会自动生成设置页面。仅当您需要超出基本表单的行为时,才使用自定义 React 页面。
导航
插件页面出现在插件名称下的管理侧边栏中。顺序与 admin.pages 数组匹配。
admin: {
pages: [
{ path: "/settings", label: "Settings", icon: "settings" }, // 第一个
{ path: "/history", label: "History", icon: "history" }, // 第二个
{ path: "/reports", label: "Reports", icon: "chart" }, // 第三个
],
}
构建配置
管理组件需要单独的构建入口点。配置您的打包器:
tsdown
export default {
entry: {
index: "src/index.ts",
admin: "src/admin.tsx",
},
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
}; tsup
export default {
entry: ["src/index.ts", "src/admin.tsx"],
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
}; 将 React 和 EmDash Admin 作为外部依赖项保留,以避免打包重复项。
插件启用/禁用
当插件在管理界面中被禁用时:
- 侧边栏链接被隐藏。
- 仪表板小部件不会被渲染。
- 管理页面返回 404。
- 后端钩子仍然执行(为了数据安全)。
插件可以检查其启用状态:
const enabled = await ctx.kv.get<boolean>("_emdash:enabled");
完整示例
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export function analyticsPlugin(): PluginDescriptor {
return {
id: "analytics",
version: "1.0.0",
format: "native",
entrypoint: "@my-org/plugin-analytics",
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [
{ path: "/dashboard", label: "Dashboard", icon: "chart" },
{ path: "/settings", label: "Settings", icon: "settings" },
],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin() {
return definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["network:request"],
allowedHosts: ["api.analytics.example.com"],
storage: {
events: { indexes: ["type", "createdAt"] },
},
admin: {
entry: "@my-org/plugin-analytics/admin",
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
enabled: { type: "boolean", label: "Enabled", default: true },
},
pages: [
{ path: "/dashboard", label: "Dashboard", icon: "chart" },
{ path: "/settings", label: "Settings", icon: "settings" },
],
widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
},
routes: {
stats: {
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const count = await ctx.storage.events.count({
createdAt: { gte: today },
});
return { today: count };
},
},
},
});
}
export default createPlugin;
import { EventsWidget } from "./components/EventsWidget";
import { DashboardPage } from "./components/DashboardPage";
import { SettingsPage } from "./components/SettingsPage";
export const widgets = {
"events-today": EventsWidget,
};
export const pages = {
"/dashboard": DashboardPage,
"/settings": SettingsPage,
};