AI 组件预览系统:实时渲染 Props 组合的智能可视化方案

📅 2026/7/14 12:43:37
AI 组件预览系统:实时渲染 Props 组合的智能可视化方案
AI 组件预览系统实时渲染 Props 组合的智能可视化方案一、Storybook 有 50 个 Story但设计师只看前 3 个一个 Button 组件的 Storybook 包含了 50 个 Story——5 种 variant × 4 种 size × 3 种 state 一些组合。每个 Story 都是前端手动编写的维护成本随着组件数量47 个组件和 Props 维度平均每个组件 8 个 Props的增长而增长。更残酷的事实是设计师只看了其中 15% 的 Story——variant 组合 核心交互状态。剩下 85% 的 Story 覆盖的是一些几乎不会被用到的边界 Props 组合。问题不在于 Story 太多——问题在于 Story 是静态的。你只能预览已经写好的 Props 组合不能在预览面板上动态调整variant→size→disabled的联动效果。设计师需要在 Figma 中切换 Variant然后跑到 Storybook 中找对应的 Story 确认——这个来回切换的认知切换成本是设计和实现之间的最后 5% 沟通损耗。AI 组件预览系统做的事是读取组件的 Props 类型定义TypeScript Interface自动推断 Props 之间的约束关系哪些组合是合法的、哪些是冲突的生成一个交互式的预览面板——滑块改size、开关切disabled、下拉选variant。所有合法的 Props 组合都能实时预览非法的组合variantprimarydisabledtrueloadingtrue也会被标记但不阻止尝试——让设计师自己看为什么这个组合不该用。二、Props 组合空间与约束推断一个组件的合法 Props 组合总数 P1 × P2 × ... × Pn其中 Pi 是第 i 个 Prop 的取值范围大小枚举型的选项数、布尔型的 2、数字型的连续范围被离散化后的采样点数。Button 组件的 Props 组合总数variant(5) × size(4) × disabled(2) × loading(2) × iconPosition(3) 240 种。如果算上onClick回调行为函数不会影响视觉实际需要渲染的组合是 240 种。这就是 AI 预览系统的价值——用一个交互面板覆盖 240 种组合而不是写 240 个 Story。约束推断是为 Props 增加互斥或前提规则。例如loadingtrue时disabled应被自动设为true视觉上显示加载状态就不该能点击。这些约束可以从组件的源代码或 JSDoc 注释中推断也可以由开发者手动标注。flowchart TD A[组件 TypeScript Interface] -- B[Props 提取器br/(ts-morph / react-docgen)] B -- C[Props 元数据br/(名称/类型/默认值/枚举值)] C -- D[Props 约束推断] D -- E{约束类型} E --|互斥| F[variantghost 时br/border 不生效] E --|联动| G[loadingtruebr/→ disabledtrue] E --|前置| H[sizelg 时br/fontSize 最小 16px] F -- I[生成交互预览面板br/(Props → 实时渲染)] G -- I H -- I三、预览系统的实现/** * AI 组件预览系统 * * 功能 * 1. 读取 TypeScript Props 接口 → 自动推断可调整的参数 * 2. 生成交互式控制面板下拉/开关/滑块/颜色选择器 * 3. 实时渲染所有 Props 组合 * * 渲染引擎使用 React 动态组件映射 */ import React, { useState, useMemo } from react; // Props 元数据 type PropType string | number | boolean | enum | color | ReactNode; interface PropMeta { name: string; type: PropType; defaultValue: unknown; enumValues?: string[]; min?: number; // number 类型的最小值 max?: number; // number 类型的最大值 description?: string; } interface ComponentMeta { name: string; props: PropMeta[]; constraints?: Constraint[]; } interface Constraint { type: mutual-exclusion | cascade | prerequisite; when: Recordstring, unknown; // 触发条件{ variant: ghost } then: Recordstring, unknown; // 结果{ border: false } description: string; } // Button 组件元数据 const ButtonMeta: ComponentMeta { name: Button, props: [ { name: variant, type: enum, defaultValue: primary, enumValues: [primary, secondary, outline, ghost, link] }, { name: size, type: enum, defaultValue: md, enumValues: [xs, sm, md, lg, xl] }, { name: disabled, type: boolean, defaultValue: false }, { name: loading, type: boolean, defaultValue: false }, { name: children, type: ReactNode, defaultValue: 按钮 }, { name: fullWidth, type: boolean, defaultValue: false }, { name: iconPosition, type: enum, defaultValue: none, enumValues: [left, right, none] }, { name: borderRadius, type: number, defaultValue: 8, min: 0, max: 24 }, ], }; /** * 约束推断引擎 * 从 Props 元数据中推断规则 */ function inferConstraints(meta: ComponentMeta): Constraint[] { const constraints: Constraint[] []; const props meta.props; // 规则 1loading 时自动 disabled const hasLoading props.find((p) p.name loading); const hasDisabled props.find((p) p.name disabled); if (hasLoading hasDisabled) { constraints.push({ type: cascade, when: { loading: true }, then: { disabled: true }, description: Loading 状态下按钮自动禁用防止重复提交, }); } // 规则 2variantghost 或 link 时无 border const hasVariant props.find((p) p.name variant); if (hasVariant?.enumValues?.includes(ghost)) { constraints.push({ type: cascade, when: { variant: ghost }, then: { border: none }, description: Ghost 和 Link 变体无边框视觉一致性, }); } return constraints; } /** * 预览面板组件 * param component - 实际渲染的组件 * param meta - Props 元数据 */ function PropsPreviewPanel({ component: Component, meta, }: { component: React.ComponentTypeany; meta: ComponentMeta; }) { // 初始化 Props 为默认值 const [props, setProps] useStateRecordstring, unknown(() { const defaults: Recordstring, unknown {}; meta.props.forEach((p) { defaults[p.name] p.defaultValue; }); return defaults; }); // 计算约束当前 Props 是否触发约束 const constraints useMemo(() inferConstraints(meta), [meta]); const activeConstraints useMemo(() { return constraints.filter((c) { return Object.entries(c.when).every(([key, value]) props[key] value); }); }, [props, constraints]); // Props 更新处理 const updateProp (name: string, value: unknown) { setProps((prev) { const next { ...prev, [name]: value }; // 应用级联约束 for (const c of constraints) { if (c.type cascade Object.entries(c.when).every(([k, v]) next[k] v)) { Object.assign(next, c.then); } } return next; }); }; return ( div style{{ display: flex, gap: 24, padding: 24 }} {/* 左侧控制面板 */} div style{{ width: 280, flexShrink: 0 }} h3{meta.name} 属性面板/h3 {meta.props.map((prop) ( div key{prop.name} style{{ marginBottom: 16 }} label style{{ display: block, fontSize: 12, color: #64748b, marginBottom: 4 }} {prop.name} {prop.description ? — ${prop.description} : } /label {renderControl(prop, props[prop.name], (v) updateProp(prop.name, v))} /div ))} {/* 活跃约束提示 */} {activeConstraints.length 0 ( div style{{ marginTop: 16, padding: 12, background: #fef3c7, borderRadius: 8, fontSize: 12 }} strong⚠️ 活跃约束/strong {activeConstraints.map((c, i) ( div key{i}{c.description}/div ))} /div )} /div {/* 右侧组件预览 多尺寸视口 */} div style{{ flex: 1 }} div style{{ marginBottom: 16, display: flex, gap: 8 }} {[375, 768, 1024, 1440].map((w) ( button key{w} style{{ padding: 4px 12px, border: 1px solid #e2e8f0, borderRadius: 4, fontSize: 12, cursor: pointer, }} {w}px /button ))} /div div style{{ border: 1px solid #e2e8f0, borderRadius: 8, padding: 40, display: flex, alignItems: center, justifyContent: center, minHeight: 200, background: props[disabled] ? #f8fafc : #fff, }} Component {...props} / /div /div /div ); } /** * 根据 Prop 类型渲染对应的表单控件 */ function renderControl( prop: PropMeta, value: unknown, onChange: (v: unknown) void ): React.ReactNode { switch (prop.type) { case boolean: return ( label style{{ display: flex, alignItems: center, gap: 8, cursor: pointer }} input typecheckbox checked{Boolean(value)} onChange{(e) onChange(e.target.checked)} / {value ? ✅ true : ⬜ false} /label ); case enum: return ( select value{String(value)} onChange{(e) onChange(e.target.value)} style{{ width: 100%, padding: 4px 8px, borderRadius: 4, border: 1px solid #e2e8f0 }} {prop.enumValues?.map((v) ( option key{v} value{v}{v}/option ))} /select ); case number: return ( div style{{ display: flex, alignItems: center, gap: 8 }} input typerange min{prop.min || 0} max{prop.max || 100} value{Number(value)} onChange{(e) onChange(Number(e.target.value))} style{{ flex: 1 }} / span style{{ fontSize: 12, minWidth: 30 }}{String(value)}/span /div ); case string: return ( input typetext value{String(value)} onChange{(e) onChange(e.target.value)} style{{ width: 100%, padding: 4px 8px, borderRadius: 4, border: 1px solid #e2e8f0 }} / ); default: return span{String(value)}/span; } } export { PropsPreviewPanel, ComponentMeta, PropMeta, ButtonMeta, inferConstraints };四、预览系统的边界组合爆炸的采样策略。240 种组合中最关键的可能是 20 种——variant(5) × size(4)。剩下的disabled×loading×iconPosition 8 种组合只是对这 20 种基础的状态修饰。交互预览让设计师可以在 20 种基础组合上切换 8 种状态——这比 50 个静态 Story 更灵活但比写一个遍历 240 种并截图的测试脚本需要的计算资源少得多。跨组件交互的盲区。预览面板只渲染单个组件——它不展示两个 Button 并排一个 primary 一个 secondary间距 16px时的全局视觉效果。组件间的组合和上下文关系在任何单组件预览中都是盲区。这需要页面级预览系统来覆盖——不是本文的范围。五、总结AI 组件预览系统 Props 元数据提取 约束推断 交互控制面板 实时渲染。Props 元数据从 TypeScript Interface 提取react-docgen / ts-morph生成类型、默认值、枚举值。约束推断自动识别loading → disabled 级联、ghost variant → 无边框等规则。一个交互面板覆盖 240 种 Props 组合——比 50 个静态 Story 更灵活。预览面板支持多尺寸视口切换375/768/1024/1440——覆盖响应式检查。非法的 Props 组合不阻止尝试——用警告提示为什么这个组合不该用。约束系统支持四种规则互斥mutual-exclusion、级联cascade、前置条件prerequisite、提示advisory。单组件预览是点检查——跨组件的上下文关系需要页面级预览来覆盖。预览系统的目标是消除Figma 切 Variant → Storybook 找 Story的认知切换。Props 面板的设计原则默认值能立即渲染修改值能立即反馈约束冲突能立即提示。