diff --git a/examples/playground/src/layouts/dashboard.tsx b/examples/playground/src/layouts/dashboard.tsx new file mode 100644 index 0000000..e2d02cb --- /dev/null +++ b/examples/playground/src/layouts/dashboard.tsx @@ -0,0 +1,24 @@ +/** + * dashboard 布局的真实渲染骨架 —— 数据看板 + * 槽位:stats(顶部指标条)/ content(主内容) + */ +import React from 'react' +import type { LayoutRenderProps } from 'vibe-registry' + +export function DashboardLayout({ slots, blocks, page }: LayoutRenderProps): React.ReactElement { + const stats = (blocks['stats'] as React.ReactNode[] | undefined) ?? [] + const content = (blocks['content'] as React.ReactNode[] | undefined) ?? [] + + return ( +
+
+ {page.title ?? 'Dashboard'} + layout: {slots.join(' + ')} +
+
+
{stats}
+
{content}
+
+
+ ) +} diff --git a/examples/playground/src/main.tsx b/examples/playground/src/main.tsx index 285f3ca..09db5ed 100644 --- a/examples/playground/src/main.tsx +++ b/examples/playground/src/main.tsx @@ -1,6 +1,5 @@ /** - * playground 入口:画廊 + 真实页面渲染(类 Vite 首页体验) - * 演示:import 即注册 → setup 业务 → PageView 渲染 + * playground 入口:模板画廊(预览即真实)+ 业务实装示例 */ import React from 'react' import { createRoot } from 'react-dom/client' @@ -28,21 +27,26 @@ function App(): React.ReactElement {
vibe-registry playground - import 即注册 · 页面只管业务 · 预览即真实 + 布局模板 · 引入即注册 · 页面只管业务 · 预览即真实
+ {/* ① 模板画廊:每卡真实渲染,点选看完整预览 */} +
+ +
+ + {/* ② 业务实装:同一布局,页面只管业务(订单列表) */}
-

真实渲染:订单列表页(layout: admin)

+

业务实装示例:订单列表页(复用 admin 布局)

+

+ 页面只声明 layout:'admin' + blocks;数据来自 setup —— 与画廊里的 demo 用同一渲染内核 +

{data ? ( ) : (
setup 加载中…
)}
- -
- -
) } diff --git a/examples/playground/src/registry.ts b/examples/playground/src/registry.ts index 2b8a156..740eedd 100644 --- a/examples/playground/src/registry.ts +++ b/examples/playground/src/registry.ts @@ -4,21 +4,103 @@ */ import { registerLayout, registerWidget } from 'vibe-registry' import { AdminLayout } from './layouts/admin.tsx' -import { PageHeader, DataTable, NavMenu } from './widgets.tsx' +import { DashboardLayout } from './layouts/dashboard.tsx' +import { PageHeader, DataTable, NavMenu, StatGrid } from './widgets.tsx' + +// ── 布局注册(带真实渲染骨架 + demo 预览数据)────────────────── -// ── 布局注册(带真实渲染骨架)────────────────────────────── registerLayout({ kind: 'layout', id: 'admin', slots: ['sidebar', 'header', 'content', 'footer'], - description: '后台管理骨架', + description: '后台管理骨架(侧栏+顶栏+内容)', render: AdminLayout, preview: { - title: 'Admin · 后台管理', + 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' }, + { 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: '状态' }, + ], + rows: [ + { id: 'A1001', customer: '张三', amount: '¥1,299', status: '已支付' }, + { id: 'A1002', customer: '李四', amount: '¥88', status: '待发货' }, + ], + }, + }, + ], + }, +}) + +registerLayout({ + kind: 'layout', + id: 'dashboard', + slots: ['stats', 'content'], + description: '数据看板(顶部指标 + 主内容)', + render: DashboardLayout, + preview: { + title: 'Dashboard 数据看板', + demoBlocks: [ + { + slot: 'stats', + widget: 'stat-grid', + props: { + stats: [ + { label: '今日订单', value: 128, delta: '+12%' }, + { label: '销售额', value: '¥45,200', delta: '+8%' }, + { label: '失败率', value: '1.2%', delta: '-0.3%' }, + ], + }, + }, + { + slot: 'content', + widget: 'data-table', + props: { + columns: [ + { key: 'name', title: '指标' }, + { key: 'value', title: '数值' }, + ], + rows: [ + { name: '订单转化率', value: '3.4%' }, + { name: '客单价', value: '¥353' }, + ], + }, + }, + ], + }, +}) + +registerLayout({ + kind: 'layout', + id: 'app-shell', + slots: ['header', 'content'], + description: '通用外壳(顶栏+内容),无自定义 render → 走缺省兜底', + preview: { + title: 'App Shell 通用外壳', + demoBlocks: [ + { slot: 'header', widget: 'page-header', props: { title: '欢迎使用', subtitle: '无 render 的布局由 DefaultLayout 兜底' } }, + { + slot: 'content', + widget: 'data-table', + props: { + columns: [ + { key: 'k', title: '项' }, + { key: 'v', title: '值' }, + ], + rows: [ + { k: '布局', v: 'app-shell' }, + { k: '渲染', v: 'DefaultLayout 兜底' }, + ], + }, + }, ], }, }) @@ -42,3 +124,9 @@ registerWidget({ component: DataTable, description: '通用数据表格(列定义来自声明 props)', }) +registerWidget({ + kind: 'widget', + id: 'stat-grid', + component: StatGrid, + description: '指标格栅(仪表盘统计)', +}) diff --git a/examples/playground/src/widgets.tsx b/examples/playground/src/widgets.tsx index 93a0347..30284d4 100644 --- a/examples/playground/src/widgets.tsx +++ b/examples/playground/src/widgets.tsx @@ -70,3 +70,33 @@ export function NavMenu({ items = [] }: { items?: Array<{ label: string; active? ), ) } + +/** 统计卡片(仪表盘用) */ +export function StatCard({ + label, + value, + delta, +}: { + label?: string + value?: unknown + delta?: string +}): React.ReactElement { + return React.createElement( + 'div', + { style: { border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, background: '#fff' } }, + React.createElement('div', { style: { fontSize: 12, color: '#6b7280' } }, label ?? ''), + React.createElement('div', { style: { fontSize: 24, fontWeight: 700, margin: '4px 0' } }, String(value ?? '-')), + delta ? React.createElement('div', { style: { fontSize: 12, color: delta.startsWith('-') ? '#dc2626' : '#16a34a' } }, delta) : null, + ) +} + +/** 指标格子容器(dashboard 布局的内容区 demo 用) */ +export function StatGrid({ stats = [] }: { stats?: Array<{ label?: string; value?: unknown; delta?: string }> }): React.ReactElement { + return React.createElement( + 'div', + { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12 } }, + ...(stats as Array<{ label?: string; value?: unknown; delta?: string }>).map((s, i) => + React.createElement(StatCard, { key: i, label: s.label, value: s.value, delta: s.delta }), + ), + ) +} diff --git a/src/render.ts b/src/render.ts index 47439e1..6f52819 100644 --- a/src/render.ts +++ b/src/render.ts @@ -99,51 +99,109 @@ export function DefaultLayout({ slots, blocks }: LayoutRenderProps): React.React ) } -/* ── 画廊:列出全部注册布局 + live 预览(类 Vite 首页)────────── */ +/* ── 画廊:布局模板 live 预览(类 Vite 首页,点选查看) ─────────── */ export interface GalleryProps { readonly registry: RegistryApi readonly pages?: readonly PageDeclaration[] } -function PreviewShell({ title, children }: { title: string; children?: React.ReactNode }): React.ReactElement { +/** 由布局的 preview.demoBlocks 构造一个可渲染的 demo 页声明 */ +function demoPageFrom(layout: LayoutRegistryEntry): PageDeclaration | null { + const demo = layout.preview?.demoBlocks ?? [] + if (demo.length === 0) return null + return { + layout: layout.id, + title: layout.preview?.title ?? layout.id, + blocks: demo.map((d) => ({ widget: d.widget, slot: d.slot, props: d.props })), + } +} + +/** 缩略预览:把真实渲染缩放进卡片(预览即真实) */ +function Thumb({ children, onClick }: { children?: React.ReactNode; onClick?: () => void }): 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), + { + onClick, + style: { + width: 420, + height: 260, + overflow: 'hidden', + border: '1px solid #e5e7eb', + borderRadius: 8, + cursor: onClick ? 'pointer' : 'default', + position: 'relative', + background: '#fff', + }, + }, + React.createElement( + 'div', + { style: { width: 1050, transform: 'scale(0.4)', transformOrigin: 'top left' } }, + 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 ?? [] + const [selected, setSelected] = React.useState(null) + + const cards = layouts.map((layout) => { + const page = demoPageFrom(layout) return React.createElement( - PreviewShell, - { key: layout.id, title: `${layout.preview?.title ?? layout.id} (${layout.id})` }, + 'div', + { key: layout.id, style: { display: 'flex', flexDirection: 'column', gap: 6 } }, + React.createElement( + Thumb, + { onClick: () => setSelected(layout) }, + page + ? React.createElement(PageView, { page, registry, data: {} }) + : React.createElement('div', { style: { padding: 12, color: '#9ca3af' } }, '(无 demoBlocks)'), + ), 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)'), + { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, + React.createElement( + 'div', + {}, + React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, layout.preview?.title ?? layout.id), + React.createElement('div', { style: { fontSize: 11, color: '#9ca3af' } }, `id: ${layout.id} · slots: ${layout.slots.join(' → ')}`), + ), + React.createElement( + 'button', + { + onClick: () => setSelected(layout), + style: { fontSize: 12, padding: '3px 10px', borderRadius: 6, border: '1px solid #d1d5db', background: '#fff', cursor: 'pointer' }, + }, + '预览', + ), ), ) }) + + const detail = selected ? demoPageFrom(selected) : null + const detailEl = selected + ? React.createElement( + 'div', + { style: { marginTop: 20 } }, + React.createElement('h3', {}, `${selected.preview?.title ?? selected.id} · 完整预览`), + detail + ? React.createElement( + 'div', + { style: { border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' } }, + React.createElement(PageView, { page: detail, registry, data: {} }), + ) + : React.createElement('div', {}, '(此布局无 demo)'), + ) + : null + 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), + { style: { fontFamily: 'system-ui, sans-serif', maxWidth: 960, 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', gridTemplateColumns: 'repeat(auto-fill, minmax(430px, 1fr))', gap: 16 } }, ...cards), + detailEl, ) }