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:
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* admin 布局的真实渲染骨架
|
||||
* layout id 是稳定契约:页面只引用 'admin',骨架长什么样由这里决定
|
||||
*/
|
||||
import React from 'react'
|
||||
import type { LayoutRenderProps } from 'vibe-registry'
|
||||
|
||||
export function AdminLayout({ blocks }: LayoutRenderProps): React.ReactElement {
|
||||
const sidebar = (blocks['sidebar'] as React.ReactNode[] | undefined) ?? []
|
||||
const header = (blocks['header'] as React.ReactNode[] | undefined) ?? []
|
||||
const content = (blocks['content'] as React.ReactNode[] | undefined) ?? []
|
||||
const footer = (blocks['footer'] as React.ReactNode[] | undefined) ?? []
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, sans-serif' }}>
|
||||
<aside style={{ width: 200, background: '#1f2937', color: '#f9fafb', padding: 16, flexShrink: 0 }}>
|
||||
<div style={{ fontWeight: 700, marginBottom: 12 }}>vibe-admin</div>
|
||||
{sidebar}
|
||||
</aside>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ padding: '12px 24px', borderBottom: '1px solid #e5e7eb', background: '#fff' }}>{header}</div>
|
||||
<main style={{ padding: 24, background: '#f9fafb', flex: 1 }}>{content}</main>
|
||||
{footer.length > 0 ? <footer style={{ padding: 12, color: '#9ca3af', fontSize: 12 }}>{footer}</footer> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* playground 入口:画廊 + 真实页面渲染(类 Vite 首页体验)
|
||||
* 演示:import 即注册 → setup 业务 → PageView 渲染
|
||||
*/
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { registry } from 'vibe-registry'
|
||||
import { PageView, LayoutGallery } from 'vibe-registry/render'
|
||||
import { orderListPage } from './pages.ts'
|
||||
|
||||
function App(): React.ReactElement {
|
||||
const [data, setData] = React.useState<Record<string, unknown> | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
if (!orderListPage.setup) return
|
||||
const d = (await orderListPage.setup({ query: {}, params: {} })) as Record<string, unknown> | undefined
|
||||
if (!cancelled) setData(d ?? {})
|
||||
}
|
||||
void load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section style={{ borderBottom: '1px solid #e5e7eb', padding: '12px 24px', background: '#111827', color: '#fff' }}>
|
||||
<strong>vibe-registry playground</strong>
|
||||
<span style={{ marginLeft: 12, color: '#9ca3af', fontSize: 12 }}>import 即注册 · 页面只管业务 · 预览即真实</span>
|
||||
</section>
|
||||
|
||||
<section style={{ padding: 24 }}>
|
||||
<h2 style={{ marginTop: 0 }}>真实渲染:订单列表页(layout: admin)</h2>
|
||||
{data ? (
|
||||
<PageView page={orderListPage} registry={registry} data={data} />
|
||||
) : (
|
||||
<div style={{ color: '#6b7280' }}>setup 加载中…</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section style={{ padding: 24, borderTop: '1px solid #e5e7eb', background: '#f9fafb' }}>
|
||||
<LayoutGallery registry={registry} />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* playground 页面声明 —— 验证"用户告诉 agent 用 admin 布局,页面只管业务"
|
||||
*/
|
||||
import { definePage } from 'vibe-registry'
|
||||
import './registry.ts' // 集中注册(副作用)
|
||||
|
||||
export const orderListPage = definePage({
|
||||
layout: 'admin',
|
||||
title: '订单列表',
|
||||
blocks: [
|
||||
{ slot: 'sidebar', widget: 'nav-menu', props: { items: [{ label: '订单', active: true }, { label: '商品' }, { label: '客户' }] } },
|
||||
{ slot: 'header', widget: 'page-header', props: { title: '订单管理', subtitle: '近 30 天全部订单' } },
|
||||
{
|
||||
slot: 'content',
|
||||
widget: 'data-table',
|
||||
props: {
|
||||
columns: [
|
||||
{ key: 'id', title: '订单号' },
|
||||
{ key: 'customer', title: '客户' },
|
||||
{ key: 'amount', title: '金额' },
|
||||
{ key: 'status', title: '状态' },
|
||||
],
|
||||
},
|
||||
bind: { rows: 'orders' },
|
||||
},
|
||||
],
|
||||
setup: async () => {
|
||||
return {
|
||||
orders: [
|
||||
{ id: 'A1001', customer: '张三', amount: '¥ 1,299', status: '已支付' },
|
||||
{ id: 'A1002', customer: '李四', amount: '¥ 88', status: '待发货' },
|
||||
{ id: 'A1003', customer: '王五', amount: '¥ 4,560', status: '已完成' },
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* playground 集中注册:widget 挂真实组件,layout 挂真实骨架
|
||||
* 这就是"项目侧注册"的完整形态 —— 之后 AI 生成页面只用 id,不碰实现
|
||||
*/
|
||||
import { registerLayout, registerWidget } from 'vibe-registry'
|
||||
import { AdminLayout } from './layouts/admin.tsx'
|
||||
import { PageHeader, DataTable, NavMenu } from './widgets.tsx'
|
||||
|
||||
// ── 布局注册(带真实渲染骨架)──────────────────────────────
|
||||
registerLayout({
|
||||
kind: 'layout',
|
||||
id: 'admin',
|
||||
slots: ['sidebar', 'header', 'content', 'footer'],
|
||||
description: '后台管理骨架',
|
||||
render: AdminLayout,
|
||||
preview: {
|
||||
title: 'Admin · 后台管理',
|
||||
demoBlocks: [
|
||||
{ slot: 'sidebar', widget: 'nav-menu', props: { items: [{ label: '订单', active: true }, { label: '商品' }] } },
|
||||
{ slot: 'header', widget: 'page-header', props: { title: '订单管理', subtitle: '全部订单' } },
|
||||
{ slot: 'content', widget: 'data-table' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// ── widget 注册(挂真实组件)──────────────────────────────
|
||||
registerWidget({
|
||||
kind: 'widget',
|
||||
id: 'page-header',
|
||||
component: PageHeader,
|
||||
description: '页头(标题 + 副标题)',
|
||||
})
|
||||
registerWidget({
|
||||
kind: 'widget',
|
||||
id: 'nav-menu',
|
||||
component: NavMenu,
|
||||
description: '侧栏导航菜单',
|
||||
})
|
||||
registerWidget({
|
||||
kind: 'widget',
|
||||
id: 'data-table',
|
||||
component: DataTable,
|
||||
description: '通用数据表格(列定义来自声明 props)',
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* playground 业务 widget(真实 React 组件)
|
||||
* 演示:注册表 widget → React 组件 → 渲染内核装配
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
/** 页头 */
|
||||
export function PageHeader({ title, subtitle }: { title?: string; subtitle?: string }): React.ReactElement {
|
||||
return React.createElement(
|
||||
'header',
|
||||
{ style: { marginBottom: 16 } },
|
||||
React.createElement('h1', { style: { margin: '0 0 4px', fontSize: 20 } }, title ?? '未命名'),
|
||||
subtitle ? React.createElement('p', { style: { margin: 0, color: '#6b7280', fontSize: 13 } }, subtitle) : null,
|
||||
)
|
||||
}
|
||||
|
||||
/** 数据表格 */
|
||||
export function DataTable({
|
||||
columns = [],
|
||||
rows = [],
|
||||
}: {
|
||||
columns?: Array<{ key: string; title: string }>
|
||||
rows?: Array<Record<string, unknown>>
|
||||
}): React.ReactElement {
|
||||
return React.createElement(
|
||||
'table',
|
||||
{ style: { borderCollapse: 'collapse', width: '100%', fontSize: 13 } },
|
||||
React.createElement(
|
||||
'thead',
|
||||
{},
|
||||
React.createElement(
|
||||
'tr',
|
||||
{},
|
||||
...(columns as Array<{ key: string; title: string }>).map((c) =>
|
||||
React.createElement(
|
||||
'th',
|
||||
{ key: c.key, style: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #d1d5db' } },
|
||||
c.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
React.createElement(
|
||||
'tbody',
|
||||
{},
|
||||
...(rows as Array<Record<string, unknown>>).map((r, i) =>
|
||||
React.createElement(
|
||||
'tr',
|
||||
{ key: i },
|
||||
...(columns as Array<{ key: string; title: string }>).map((c) =>
|
||||
React.createElement('td', { key: c.key, style: { padding: '6px 8px', borderBottom: '1px solid #f3f4f6' } }, String(r[c.key] ?? '')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** 导航菜单 */
|
||||
export function NavMenu({ items = [] }: { items?: Array<{ label: string; active?: boolean }> }): React.ReactElement {
|
||||
return React.createElement(
|
||||
'ul',
|
||||
{ style: { listStyle: 'none', margin: 0, padding: 0 } },
|
||||
...(items as Array<{ label: string; active?: boolean }>).map((it, i) =>
|
||||
React.createElement(
|
||||
'li',
|
||||
{ key: i, style: { padding: '6px 8px', borderRadius: 6, marginBottom: 2, background: it.active ? '#e0e7ff' : 'transparent' } },
|
||||
it.label,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user