Repository navigation
Frontend Getting Started
worable edited this page Oct 12, 2026
·
1 revision
做一个最小的插件前端,包含一个公开页面、一个 Finder 和一个管理后台页面,跑通从写代码到在后台看到效果的完整链路。字段细节先不用记,跑通后再查前端清单。
pnpm plugin:create my-plugin --name "我的插件" --frontend会生成:
packages/plugins/my-plugin/
├── src/index.ts # 插件后端(definePlugin)
├── frontend/
│ ├── package.json
│ └── src/
│ ├── index.tsx # 服务端入口:页面 / Finder / 路由声明
│ └── admin.tsx # 客户端入口:交互式管理后台页面
├── tsconfig.json
└── package.json
两个前端入口的分工,是理解这套系统的关键:
-
index.tsx在服务端渲染。放公开页面(pages)、数据源(finders),以及路由声明(ui.adminRoutes)。 -
admin.tsx在客户端渲染(文件首行'use client')。放交互式管理后台页面组件(adminPages)。
打开 frontend/src/index.tsx,脚手架已经写好了一个公开页面和一个 Finder:
import type { FinderContext, FrontendPackage } from '@stackpanel/sdk';
import { PageHeader } from '@stackpanel/ui';
import type { ReactElement } from 'react';
import { z } from 'zod';
function MyPluginPage(): ReactElement {
return (
<main className="w-full space-y-8">
<PageHeader title="我的插件" description="公开页面。" />
</main>
);
}
const statusSchema = z.object({
plugin: z.string(),
version: z.string(),
status: z.string(),
});
const finders = {
'my-plugin.status': async (_input: Record<string, unknown>, ctx: FinderContext) =>
ctx.api.get('/admin/my-plugin/status', statusSchema),
};
export const frontend: FrontendPackage = {
pages: [{ path: '/my-plugin', component: 'my-plugin/index' }],
pageComponents: { 'my-plugin/index': MyPluginPage },
finders,
ui: {
adminRoutes: [
{
path: '/overview',
component: 'admin/overview', // 名字要和 admin.tsx 的 adminPages 键一致
permission: 'my-plugin:admin',
nav: { label: '我的插件', group: '插件' },
data: [{ finder: 'my-plugin.status', as: 'status' }],
},
],
},
};
// 必须保留:平台靠 default 导出解析本包。
export default frontend;要点:
-
pages[].component是组件在pageComponents里的键,不是函数名。 -
data声明了页面要预先取哪些数据:{ finder, as }表示用my-plugin.status这个 Finder,把结果注入组件的props.data.status。 -
ui.adminRoutes只声明路由;组件不在这里。
打开 frontend/src/admin.tsx:
'use client';
import type { AdminPageComponentProps } from '@stackpanel/sdk';
import { PageHeader } from '@stackpanel/ui';
import type { ReactElement } from 'react';
import { z } from 'zod';
const statusSchema = z.object({
plugin: z.string(),
version: z.string(),
status: z.string(),
});
function MyPluginAdminPage(props: AdminPageComponentProps): ReactElement {
const status = props.data.status as z.infer<typeof statusSchema> | undefined;
return (
<main className="w-full space-y-8">
<PageHeader title="我的插件" description="管理入口。" />
{status ? (
<dl className="grid gap-3 rounded-lg border bg-card p-4 text-sm sm:grid-cols-3">
<div>
<dt className="text-muted-foreground">版本</dt>
<dd className="mt-1 font-mono">{status.version}</dd>
</div>
<div>
<dt className="text-muted-foreground">状态</dt>
<dd className="mt-1">{status.status}</dd>
</div>
</dl>
) : null}
</main>
);
}
export const adminPages = {
'admin/overview': MyPluginAdminPage, // 键必须匹配 ui.adminRoutes[].component
} as const;要点:
- 首行必须是
'use client'。 - 导出名必须是
adminPages,类型为Record<component名, 组件>。 - 它同样通过
props.data拿到adminRoutes[].data里声明的数据。
pnpm build:frontend这一步会:
- 把
src/index.tsx编成frontend/dist/index.js,src/admin.tsx编成frontend/dist/admin.js; - 重新生成
frontend/manifest.json(不要手改它); - 把产物分发到
data/plugins/<id>/frontend/,并更新apps/web/src/lib/frontend-registry.ts与frontend-client-registry.ts。
开发内置插件时,改完后按顺序执行:
pnpm --filter @stackpanel/plugin-my-plugin typecheck # 可选,先过类型
pnpm build:frontend # 重新编译前端
# 若改了后端 src/index.ts:
pnpm --filter @stackpanel/plugin-my-plugin build
# 触发 API 重新 seed
touch apps/api/src/app.ts然后在浏览器打开:
- 公开页面:
/my-plugin - 管理后台页面:
/admin/plugin/my-plugin/overview(需管理员登录)
能正常看到内容,就说明两个入口都接通了。
打包上传给外部使用时执行 pnpm plugin:pack my-plugin,再到管理后台 /admin/plugins 上传启用。
纯展示之外,最常见的需求是提交。平台用**声明式操作(actions)**统一处理鉴权、输入校验、审计与转发,你在前端只写 action={executor},后端只提供对应路由。
第一步,在 src/index.tsx 的 ui.actions 里声明:
ui: {
actions: [
{
id: 'my-plugin.create',
method: 'POST',
path: '/admin/my-plugin/items',
permission: 'my-plugin:admin',
input: [{ name: 'name', type: 'string', required: true, maxLength: 191 }],
},
],
adminRoutes: [/* ... */],
},第二步,在 admin.tsx 里把它接到表单上:
function MyPluginAdminPage(props: AdminPageComponentProps): ReactElement {
const create = props.actions['my-plugin.create'];
return (
<form action={create} className="grid gap-3">
<input name="name" required className="h-9 rounded-lg border bg-background px-3" />
<button type="submit" className="h-9 w-fit rounded-lg bg-primary px-3 text-primary-foreground">
创建
</button>
</form>
);
}props.actions['<action id>'] 是一个可直接传给原生 <form action> 的 Server Action。详见管理后台扩展。
核心开发
插件开发
主题开发
前端包开发
RESTful API