原生外掛可以透過自訂 React 頁面和儀表板小工具擴充管理面板——沙盒外掛則使用 Block Kit 描述其 UI,因為將外掛 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 下宣告每個頁面,包含路徑、標籤和圖示:
admin: {
pages: [
{
path: "/settings",
label: "Settings",
icon: "settings",
},
{
path: "/reports",
label: "Reports",
icon: "chart",
},
],
}
請用英文宣告標籤。管理介面在呈現側邊欄和命令面板之前,會透過其共享的 Lingui 實例處理標籤,因此載入自己訊息目錄的外掛(使用英文標籤作為訊息 ID)可以免費獲得本地化導覽。與管理介面自身訊息(Settings、Dashboard 等)相符的標籤即使沒有外掛目錄也會使用管理介面的翻譯;沒有目錄項目的標籤按宣告時的樣子呈現。
import { i18n } from "@lingui/core";
// 將外掛的編譯目錄合併到管理介面的 i18n 實例中。
// 當管理介面的語系為德語時,"Reports" 現在呈現為 "Berichte"。
const catalogs: Record<string, Record<string, string>> = {
de: { Reports: "Berichte", Settings: "Einstellungen" },
};
function mergeCatalog() {
const messages = catalogs[i18n.locale];
if (messages && !("Reports" in i18n.messages)) i18n.load(i18n.locale, messages);
}
mergeCatalog();
// 管理介面的語系切換器在變更時會*取代*目錄,因此需要重新合併。
// 上面的哨兵檢查防止遞迴(load() 會觸發 "change")。
i18n.on("change", mergeCatalog);
頁面元件
以下元件透過外掛 API 鉤子讀取和儲存設定:
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>外掛設定</h1>
<label>
網站標題
<input
type="text"
value={(settings.siteTitle as string) || ""}
onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
/>
</label>
<button onClick={handleSave} disabled={saving}>
{saving ? "儲存中..." : "儲存設定"}
</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、標題和大小:
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>
);
}
小工具大小
| 大小 | 描述 |
|---|---|
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="設定">
<Input label="API 金鑰" type="password" />
<Toggle label="啟用" defaultChecked />
<Button variant="primary">儲存</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");
完整範例
以下外掛定義了一個儀表板頁面、一個設定頁面和一個小工具,執行時期和管理進入點位於不同的檔案中。src/index.ts 檔案包含描述符和執行時期:
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;
src/admin.tsx 檔案將頁面路徑和小工具 ID 對應到其 React 元件:
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,
};