Files
hellow-da-sb 2277d55ac9 feat(render): M2 — React 渲染内核 + 布局画廊(playground)
- core 新增 render 子路径(vibe-registry/render):PageView + WidgetView + LayoutGallery
- layout 注册项支持 render 实现;缺省兜底 DefaultLayout
- widget 注册项 component 挂真实 React 组件
- PageBlockDecl 增加 slot 字段(块落在布局哪个槽位)
- playground:admin 布局真实骨架 + 组件 + 订单页,dev 预览 + 生产构建通过
- 全量回归:core/qa-orchestrator/playground typecheck 全绿
2026-09-03 00:59:31 +08:00

158 lines
6.2 KiB
TypeScript

/**
* vibe-registry core —— 万物皆注册的类型基石
*
* 设计要点(v0.5):
* - 注册项类型封闭 + id 即引用键
* - 所有注册项可带 demo 预览数据(预览即真实)
* - 纯 TS、零依赖、不接 UI(渲染内核后续接)
*/
export type RegistryKind = 'layout' | 'widget' | 'action' | 'schema' | 'theme'
/** 布局渲染函数签名(React 无关,render 层传入类型;避免 core 依赖 React) */
export interface LayoutRenderProps<S extends string = string> {
readonly slots: readonly S[]
/** slot → 已填充的 ReactNode 数组 */
readonly blocks: Record<string, readonly unknown[]>
/** 页面声明本身(含 title 等元信息) */
readonly page: { readonly layout: string; readonly title?: string }
}
/** 布局骨架:页面级结构。渲染内核据 slots 填充 blocks。 */
export interface LayoutRegistryEntry {
readonly kind: 'layout'
/** 简易 id,如 'admin' —— 用户/agent 只需说这个 id */
readonly id: string
/** 布局所含槽位顺序,渲染时 blocks 依 slot 填入 */
readonly slots: readonly string[]
/** 描述,供画廊/agent 展示 */
readonly description: string
/** 布局渲染实现(slots → 页面骨架)。可选:缺省用画廊的通用骨架 */
readonly render?: (props: LayoutRenderProps) => unknown
/** demo 数据(预览即真实,与实装共用渲染内核) */
readonly preview?: LayoutPreview
}
export interface LayoutPreview {
/** 标题文本,画廊首页展示 */
readonly title: string
/** 缩略示意:每个 slot 放什么 demo block(id 或描述) */
readonly demoBlocks?: ReadonlyArray<{
readonly slot: string
readonly widget: string
readonly props?: Record<string, unknown>
}>
}
/** 业务组件注册项(映射到已有 UI 库,不重造原子组件) */
export interface WidgetRegistryEntry {
readonly kind: 'widget'
readonly id: string
/** 渲染组件(React 组件;render 层消费,core 不依赖 React 类型) */
readonly component?: unknown
/** 关联字段/列 schema id(可选) */
readonly schemaId?: string
readonly description?: string
}
/** 行为注册项:可复用的事件语义(泛型:payload/结果类型随注册保留,不丢业务语义) */
export interface ActionRegistryEntry<P = unknown, R = unknown> {
readonly kind: 'action'
readonly id: string
readonly handler: (payload: P, ctx: unknown) => R | Promise<R>
readonly description?: string
}
/** 数据/字段注册项:表单字段、表格列 */
export interface SchemaRegistryEntry {
readonly kind: 'schema'
readonly id: string
readonly fields?: readonly unknown[]
readonly description?: string
}
export type RegistryEntry =
| LayoutRegistryEntry
| WidgetRegistryEntry
| ActionRegistryEntry
| SchemaRegistryEntry
/** 注册表集合:kind → id → entry(单一事实来源) */
export type RegistryStore = {
readonly layouts: ReadonlyMap<string, LayoutRegistryEntry>
readonly widgets: ReadonlyMap<string, WidgetRegistryEntry>
readonly actions: ReadonlyMap<string, ActionRegistryEntry>
readonly schemas: ReadonlyMap<string, SchemaRegistryEntry>
}
/* ── 注册与查询 ─────────────────────────────────────────────── */
export type RegisterFn<K extends RegistryKind, E extends { readonly kind: K; readonly id: string }> =
(entry: E) => E
export interface RegistryApi {
readonly store: RegistryStore
registerLayout: RegisterFn<'layout', LayoutRegistryEntry>
registerWidget: RegisterFn<'widget', WidgetRegistryEntry>
registerAction: <P, R>(entry: ActionRegistryEntry<P, R>) => ActionRegistryEntry<P, R>
registerSchema: RegisterFn<'schema', SchemaRegistryEntry>
/** 校验:给定引用集合,返回未注册的 id 清单(空 = 通过) */
validateRefs: (refs: ReadonlyArray<{ readonly kind: RegistryKind; readonly id: string }>) => string[]
/** 画廊用的清单:全部已注册项(dev 用) */
listAll: () => RegistryEntry[]
}
/** 创建注册表(单例由 createRegistry 返回) */
export function createRegistry(): RegistryApi {
const layouts = new Map<string, LayoutRegistryEntry>()
const widgets = new Map<string, WidgetRegistryEntry>()
const actions = new Map<string, ActionRegistryEntry>()
const schemas = new Map<string, SchemaRegistryEntry>()
function register<K extends RegistryKind, E extends { readonly kind: K; readonly id: string }>(
map: Map<string, RegistryEntry>,
entry: E,
): E {
const existed = map.get(entry.id)
if (existed) {
throw new Error(`[vibe-registry] 注册冲突:${entry.kind} '${entry.id}' 已存在(万物皆注册,id 唯一)`)
}
map.set(entry.id, entry as RegistryEntry)
return entry
}
return {
store: { layouts, widgets, actions, schemas },
registerLayout: (e) => register(layouts, e),
registerWidget: (e) => register(widgets, e),
registerAction: <P, R>(entry: ActionRegistryEntry<P, R>) => {
if (actions.has(entry.id)) {
throw new Error(`[vibe-registry] 注册冲突:action '${entry.id}' 已存在(万物皆注册,id 唯一)`)
}
actions.set(entry.id, entry as ActionRegistryEntry)
return entry
},
registerSchema: (e) => register(schemas, e),
validateRefs(refs) {
const missing: string[] = []
for (const r of refs) {
const map = r.kind === 'layout' ? layouts : r.kind === 'widget' ? widgets : r.kind === 'action' ? actions : schemas
if (!map.has(r.id)) missing.push(`${r.kind}:${r.id}`)
}
return missing
},
listAll() {
return [...layouts.values(), ...widgets.values(), ...actions.values(), ...schemas.values()]
},
}
}
/* ── 全局注册表(单例,项目内 import 即用) ────────────────────── */
const globalRegistry = createRegistry()
export const registerLayout: RegistryApi['registerLayout'] = (e) => globalRegistry.registerLayout(e)
export const registerWidget: RegistryApi['registerWidget'] = (e) => globalRegistry.registerWidget(e)
export const registerAction: RegistryApi['registerAction'] = (e) => globalRegistry.registerAction(e)
export const registerSchema: RegistryApi['registerSchema'] = (e) => globalRegistry.registerSchema(e)
export const registry: RegistryApi = globalRegistry