feat(render): 布局模板画廊 —— live 缩略预览 + 点选完整预览
- LayoutGallery 升级:每模板卡片 = 真实渲染缩略图(scale 0.4),点选看完整预览 - demoPageFrom:由 preview.demoBlocks 构造可渲染 demo 页(预览即真实) - playground 新增 dashboard 布局 + stat-grid widget;app-shell 验证 DefaultLayout 兜底 - 三个模板(admin/dashboard/app-shell)全部 live 渲染验证通过
This commit is contained in:
+82
-24
@@ -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<LayoutRegistryEntry | null>(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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user