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 全绿
This commit is contained in:
+3
-2
@@ -8,6 +8,7 @@ export type {
|
||||
ActionRegistryEntry,
|
||||
SchemaRegistryEntry,
|
||||
LayoutPreview,
|
||||
} from './registry.js'
|
||||
LayoutRenderProps,
|
||||
} from './registry.ts'
|
||||
export { definePage } from './page.ts'
|
||||
export type { PageDeclaration, PageBlockDecl, PageContext, SetupData } from './page.js'
|
||||
export type { PageDeclaration, PageBlockDecl, PageContext, SetupData } from './page.ts'
|
||||
|
||||
@@ -12,6 +12,8 @@ import { type RegistryApi, registry } from './registry.ts'
|
||||
export interface PageBlockDecl {
|
||||
/** widget 注册 id,如 'data-table'、'page-header' */
|
||||
readonly widget: string
|
||||
/** 落在布局的哪个槽位(缺省 'content';渲染内核按布局 slots 装配) */
|
||||
readonly slot?: string
|
||||
/** 静态 props(可选) */
|
||||
readonly props?: Record<string, unknown>
|
||||
/** 业务绑定:setup 返回的数据里取哪些字段喂给 widget(键:widget prop 名,值:setup 结果字段) */
|
||||
|
||||
+12
-1
@@ -9,6 +9,15 @@
|
||||
|
||||
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'
|
||||
@@ -18,6 +27,8 @@ export interface LayoutRegistryEntry {
|
||||
readonly slots: readonly string[]
|
||||
/** 描述,供画廊/agent 展示 */
|
||||
readonly description: string
|
||||
/** 布局渲染实现(slots → 页面骨架)。可选:缺省用画廊的通用骨架 */
|
||||
readonly render?: (props: LayoutRenderProps) => unknown
|
||||
/** demo 数据(预览即真实,与实装共用渲染内核) */
|
||||
readonly preview?: LayoutPreview
|
||||
}
|
||||
@@ -37,7 +48,7 @@ export interface LayoutPreview {
|
||||
export interface WidgetRegistryEntry {
|
||||
readonly kind: 'widget'
|
||||
readonly id: string
|
||||
/** 渲染组件。内核薄,仅做映射层 */
|
||||
/** 渲染组件(React 组件;render 层消费,core 不依赖 React 类型) */
|
||||
readonly component?: unknown
|
||||
/** 关联字段/列 schema id(可选) */
|
||||
readonly schemaId?: string
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* vibe-registry render —— 极薄 React 渲染内核(可选子路径:vibe-registry/render)
|
||||
*
|
||||
* 设计(v0.5):
|
||||
* - core(registry/page)零依赖;渲染内核单独子路径,谁用谁引 React
|
||||
* - 不做运行时编辑器,不重造组件:widget.component 映射已有组件
|
||||
* - 预览即真实:画廊与实装共用本内核
|
||||
*/
|
||||
import React from 'react'
|
||||
import type { RegistryApi, LayoutRegistryEntry, LayoutRenderProps } from './registry.ts'
|
||||
import type { PageDeclaration, PageBlockDecl } from './page.ts'
|
||||
|
||||
/** widget 的 React 组件形态(宽松:props 由声明 props/bind 决定) */
|
||||
export type WidgetComponent = React.ComponentType<Record<string, unknown>> | React.ElementType
|
||||
|
||||
/* ── 单 widget 渲染 ─────────────────────────────────────────── */
|
||||
|
||||
export interface WidgetViewProps {
|
||||
readonly widget: string
|
||||
readonly registry: RegistryApi
|
||||
/** 静态 props */
|
||||
readonly props?: Record<string, unknown>
|
||||
/** setup 数据(供 bind 取字段) */
|
||||
readonly data?: Record<string, unknown>
|
||||
readonly bind?: Record<string, string>
|
||||
}
|
||||
|
||||
export function WidgetView({ widget, registry, props = {}, data = {}, bind }: WidgetViewProps): React.ReactElement {
|
||||
const entry = registry.store.widgets.get(widget)
|
||||
if (!entry) {
|
||||
throw new Error(`[vibe-registry/render] 未知 widget:'${widget}' (未注册)`)
|
||||
}
|
||||
const Comp = entry.component as WidgetComponent | undefined
|
||||
if (!Comp) {
|
||||
throw new Error(`[vibe-registry/render] widget '${widget}' 未挂组件(component 缺失)`)
|
||||
}
|
||||
// bind:props 名 ← setup 数据字段
|
||||
const bound: Record<string, unknown> = {}
|
||||
if (bind) {
|
||||
for (const [prop, key] of Object.entries(bind)) bound[prop] = data[key]
|
||||
}
|
||||
return React.createElement(Comp, { ...props, ...bound })
|
||||
}
|
||||
|
||||
/* ── 整页渲染:声明 + 注册表 → 组件树 ────────────────────────── */
|
||||
|
||||
export interface PageViewProps {
|
||||
readonly page: PageDeclaration
|
||||
readonly registry: RegistryApi
|
||||
/** setup 已执行完的数据 */
|
||||
readonly data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function PageView({ page, registry, data = {} }: PageViewProps): React.ReactElement {
|
||||
const layout = registry.store.layouts.get(page.layout)
|
||||
if (!layout) {
|
||||
throw new Error(`[vibe-registry/render] 未知布局:'${page.layout}'`)
|
||||
}
|
||||
const renderLayout = (layout.render as React.ComponentType<LayoutRenderProps> | undefined) ?? DefaultLayout
|
||||
// 按 slot 分组填充 blocks
|
||||
const filled: Record<string, React.ReactNode[]> = {}
|
||||
for (const b of page.blocks) {
|
||||
const slot = b.slot ?? 'content'
|
||||
;(filled[slot] ??= []).push(
|
||||
React.createElement(WidgetView, {
|
||||
key: b.widget + JSON.stringify(b.props ?? {}),
|
||||
widget: b.widget,
|
||||
registry,
|
||||
props: b.props,
|
||||
bind: b.bind,
|
||||
data,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return React.createElement(renderLayout, { slots: layout.slots, blocks: filled, page })
|
||||
}
|
||||
|
||||
/** 缺省布局骨架:无 render 的布局按 slots 顺序纵向排(演示/兜底用) */
|
||||
export function DefaultLayout({ slots, blocks }: LayoutRenderProps): React.ReactElement {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: { border: '1px dashed #d1d5db', borderRadius: 8, padding: 12 } },
|
||||
...slots.flatMap((slot) => {
|
||||
const children = blocks[slot] as React.ReactNode[] | undefined
|
||||
if (!children || children.length === 0) return []
|
||||
return [
|
||||
React.createElement(
|
||||
'div',
|
||||
{ key: slot, style: { marginBottom: 12 } },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: { fontSize: 11, color: '#9ca3af', marginBottom: 4, textTransform: 'uppercase' } },
|
||||
`slot: ${slot}`,
|
||||
),
|
||||
...children,
|
||||
),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/* ── 画廊:列出全部注册布局 + live 预览(类 Vite 首页)────────── */
|
||||
|
||||
export interface GalleryProps {
|
||||
readonly registry: RegistryApi
|
||||
readonly pages?: readonly PageDeclaration[]
|
||||
}
|
||||
|
||||
function PreviewShell({ title, children }: { title: string; children?: React.ReactNode }): React.ReactElement {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: { border: '1px solid #e5e7eb', borderRadius: 8, padding: 12, background: '#fff' } },
|
||||
React.createElement('h3', { style: { margin: '0 0 8px', fontSize: 14 } }, title),
|
||||
React.createElement('div', { style: { fontSize: 12, color: '#6b7280' } }, children),
|
||||
)
|
||||
}
|
||||
|
||||
export function LayoutGallery({ registry }: GalleryProps): React.ReactElement {
|
||||
const layouts = registry.listAll().filter((e): e is LayoutRegistryEntry => e.kind === 'layout')
|
||||
const widgets = registry.listAll().filter((e) => e.kind === 'widget')
|
||||
const items = layouts.map((layout) => {
|
||||
const demo = layout.preview?.demoBlocks ?? []
|
||||
return React.createElement(
|
||||
PreviewShell,
|
||||
{ key: layout.id, title: `${layout.preview?.title ?? layout.id} (${layout.id})` },
|
||||
React.createElement(
|
||||
'div',
|
||||
{},
|
||||
React.createElement('div', {}, `slots: ${layout.slots.join(' → ')}`),
|
||||
demo.length
|
||||
? React.createElement(
|
||||
'ul',
|
||||
{},
|
||||
demo.map((d, i) =>
|
||||
React.createElement('li', { key: i }, `${d.slot} ← widget:${d.widget} ${d.props ? JSON.stringify(d.props) : ''}`),
|
||||
),
|
||||
)
|
||||
: React.createElement('div', {}, '(无 demo)'),
|
||||
),
|
||||
)
|
||||
})
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: { fontFamily: 'system-ui, sans-serif', maxWidth: 720, margin: '0 auto', padding: 24 } },
|
||||
React.createElement('h1', { style: { fontSize: 20 } }, 'vibe-registry · 布局画廊'),
|
||||
React.createElement('p', { style: { color: '#6b7280' } }, `已注册:${layouts.length} 布局 · ${widgets.length} widget`),
|
||||
React.createElement('div', { style: { display: 'grid', gap: 12 } }, ...items),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user