跳转到内容

管理面板

EmDash 管理面板是一个嵌入在您的 Astro 站点中的 React 单页应用。它为编辑和管理员提供了一个完整的内容管理界面。

┌────────────────────────────────────────────────────────────────┐
│ Astro Shell │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React SPA │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ Components │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ REST API Client │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

管理面板是一个“大岛屿”式的 React 应用。Astro 负责外壳和身份验证;管理面板内部的所有导航和渲染都在客户端进行。

层级技术用途
路由TanStack Router类型安全的客户端路由
数据TanStack Query服务器状态、缓存、变更
UIKumo无障碍组件(基于 Base UI + Tailwind)
表格TanStack Table排序、筛选、分页
表单React Hook Form + Zod与服务器模式匹配的验证
图标Phosphor一致的图标系统
编辑器TipTap富文本编辑(Portable Text)

管理面板挂载在 /_emdash/admin/ 路径下,并使用客户端路由:

路径界面
/仪表板
/content/:collection内容列表
/content/:collection/:id内容编辑器
/content/:collection/new新建条目
/media媒体库
/content-types模式构建器(仅管理员)
/menus导航菜单
/widgets小部件区域
/taxonomies分类/标签管理
/settings站点设置
/plugins/:pluginId/*插件页面

管理面板不会硬编码关于集合或插件的知识。相反,它会从服务器获取一个清单:

GET /_emdash/api/manifest

响应:

{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" }
]
}
],
"plugins": [
{
"id": "audit-log",
"label": "Audit Log",
"adminPages": [{ "path": "history", "label": "Audit History" }],
"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
}
],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}

管理面板完全根据此清单构建其导航、表单和编辑器。优点:

  • 模式变更立即生效 —— 无需重新构建管理面板
  • 插件 UI 自动集成 —— 清单中的页面和小部件
  • 边界处的类型安全 —— Zod 模式保留在服务器端
  1. 管理面板 SPA 加载 —— TanStack Router 初始化 2. 获取清单 —— TanStack Query 缓存集合/插件元数据 3. 构建导航 —— 根据清单生成侧边栏 4. 用户导航 —— 客户端路由,无页面重载 5. 获取数据 —— TanStack Query 从 REST API 请求内容 6. 渲染表单 —— 根据清单字段描述符生成字段编辑器 7. 提交更改 —— 通过 TanStack Query 进行变更,乐观更新 8. 服务器验证 —— 服务器端的 Zod 模式,错误以 JSON 形式返回

管理面板完全通过 REST API 进行通信:

方法端点目的
GET/api/content/:collection列出条目
POST/api/content/:collection创建条目
GET/api/content/:collection/:id获取条目
PUT/api/content/:collection/:id更新条目
DELETE/api/content/:collection/:id软删除条目
GET/api/content/:collection/:id/revisions列出修订版本
POST/api/content/:collection/:id/preview-url生成预览 URL
方法端点目的
GET/api/schema导出完整模式
GET/api/schema/collections列出集合
POST/api/schema/collections创建集合
PUT/api/schema/collections/:slug更新集合
DELETE/api/schema/collections/:slug删除集合
POST/api/schema/collections/:slug/fields添加字段
PUT/api/schema/collections/:slug/fields/:field更新字段
DELETE/api/schema/collections/:slug/fields/:field删除字段
方法端点目的
GET/api/media列出媒体项
POST/api/media/upload-url获取签名上传 URL
POST/api/media/:id/confirm确认上传完成
DELETE/api/media/:id删除媒体项
GET/api/media/file/:key提供媒体文件
端点目的
/api/settings站点设置(GET/POST)
/api/menus/*导航菜单
/api/widget-areas/*小部件管理
/api/taxonomies/*分类术语
/api/admin/plugins/*插件状态

所有列表端点都使用基于游标的分页:

{
"items": [...],
"nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}

获取下一页:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

插件可以通过页面和仪表板小部件扩展管理面板。集成会生成一个包含静态导入的虚拟模块:

// virtual:emdash/plugin-admins (generated)
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
import * as pluginAdmin1 from "@emdash-cms/plugin-analytics/admin";
export const pluginAdmins = {
seo: pluginAdmin0,
analytics: pluginAdmin1,
};

插件页面挂载在 /_emdash/admin/plugins/:pluginId/* 路径下:

// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];

渲染位置:/_emdash/admin/plugins/seo/settings

插件可以向仪表板添加小部件:

export const widgets = [
{
id: "seo-overview",
component: SEOWidget,
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
];

管理面板外壳路由通过 Astro 中间件强制执行身份验证:

// Simplified middleware logic
export async function onRequest({ request, locals }, next) {
const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) {
if (!session?.user) {
return redirect("/_emdash/admin/login");
}
locals.user = session.user;
}
return next();
}

管理面板 SPA 本身不处理登录——这是一个设置会话 cookie 的 Astro 页面。

不同角色看到的管理面板部分不同:

角色可见部分
编辑仪表板、分配的集合、媒体
管理员+ 内容类型、所有集合、设置
开发者+ CLI 访问、生成的类型

清单端点会根据请求用户的角色过滤集合和功能。

内容编辑器根据字段定义动态生成表单:

// Simplified editor rendering
function ContentEditor({ collection, fields }) {
return (
<form>
{fields.map((field) => (
<FieldWidget
key={field.slug}
type={field.type}
label={field.label}
required={field.required}
options={field.options}
/>
))}
</form>
);
}

每种字段类型都有对应的小部件:

字段类型小部件
string文本输入框
text文本域
number数字输入框
boolean切换开关
datetime日期/时间选择器
select下拉菜单
multiSelect多选菜单
portableTextTipTap 编辑器
image媒体选择器
reference条目选择器

Portable Text 字段使用 TipTap(ProseMirror)进行编辑:

User types → TipTap (ProseMirror JSON) → Save → Portable Text (DB)
Load → Portable Text (DB) → TipTap (ProseMirror JSON) → Display

在加载/保存边界通过 portableTextToProsemirror() 和 prosemirrorToPortableText() 进行转换。

支持的块:

  • 段落、标题(H1-H6)
  • 项目符号和编号列表
  • 引用块、代码块
  • 图像(来自媒体库)
  • 链接

来自插件或导入的未知块将作为只读占位符保留。

媒体库提供:

  • 网格和列表视图
  • 按类型、日期搜索和筛选
  • 拖放上传
  • 带元数据的图像预览
  • 批量选择和删除

上传使用签名 URL 进行客户端到存储的直接上传:

  1. 请求上传 URL —— POST /api/media/upload-url 2. 直接上传 —— 客户端将文件 PUT 到签名 URL(R2/S3) 3. 确认上传 —— POST /api/media/:id/confirm 4. 服务器提取元数据 —— 尺寸、MIME 类型等。

这种方法绕过了 Workers 的请求体大小限制,并提供真实的上传进度。