生成式 UI 在 Dashboard 搭建中的降本增效:从设计稿到可交互原型

生成式 UI 在 Dashboard 搭建中的降本增效:从设计稿到可交互原型 生成式 UI 在 Dashboard 搭建中的降本增效从设计稿到可交互原型数据看板的搭建长期是前端团队的高频重复工作。不同业务线、不同角色需要的看板布局和图表组合千差万别手工开发效率严重受限。本文复盘将生成式 UI 技术引入 Dashboard 搭建流程的实践量化分析从设计稿到可交互原型的全链路效率提升。一、传统 Dashboard 的开发瓶颈某数据平台需要为运营、销售、财务三个部门提供定制化 Dashboard。以运营 Dashboard 为例一个完整看板包含 15-20 个图表卡片涵盖折线图、柱状图、饼图、数据表格、指标卡等 5 种图表类型。从需求评审到上线平均耗时 9 个工作日。瓶颈集中在三个环节布局搭建CSS Grid/Flexbox 的手写布局不同屏幕尺寸的响应式适配图表配置ECharts/AntV 的 option 配置冗长类型复杂参数众多数据对接每个图表的 API 接口格式不一致需要逐一手写数据转换函数引入生成式 UI 的目标是将布局搭建和图表配置两个环节自动化将开发时间从 4 天压缩到 2 天以内。二、生成式 UI 的架构方案方案的核心思路是将 Dashboard 的生成拆分为三个阶段设计解析 → 中间表示 → 代码生成。设计解析器负责从 Figma 设计稿中提取结构化信息// design-parser.ts — Figma 设计稿解析器 // 用途从 Figma API 返回的节点树中提取 Dashboard 的结构化描述 // 注意此实现依赖 Figma REST API需要在环境变量中配置 FIGMA_TOKEN interface ParsedCard { id: string; type: line-chart | bar-chart | pie-chart | data-table | metric-card; title: string; position: { x: number; y: number; width: number; height: number }; gridSpan: { cols: number; rows: number }; } interface DashboardIR { title: string; layout: grid-12 | grid-8 | grid-masonry; cards: ParsedCard[]; theme: { colorScheme: light | dark; primaryColor: string; borderRadius: number; gap: number; }; responsive: { breakpoints: { name: string; width: number; columns: number }[]; }; } export class FigmaDesignParser { private baseUrl https://api.figma.com/v1; /** * 解析 Figma 文件输出 Dashboard 中间表示 * param fileKey Figma 文件的唯一标识 * param nodeId 目标页面的节点 ID可选默认解析首页 */ async parse(fileKey: string, nodeId?: string): PromiseDashboardIR { const token process.env.FIGMA_TOKEN; if (!token) { throw new Error(FIGMA_TOKEN 环境变量未设置无法调用 Figma API); } // 获取文件节点数据 const endpoint nodeId ? ${this.baseUrl}/files/${fileKey}/nodes?ids${nodeId} : ${this.baseUrl}/files/${fileKey}; const response await fetch(endpoint, { headers: { X-Figma-Token: token }, }); if (!response.ok) { throw new Error(Figma API 请求失败: ${response.status} ${response.statusText}); } const data await response.json(); // 提取页面根节点 const rootNode nodeId ? data.nodes[nodeId]?.document : data.document.children?.[0]; // 第一个页面 if (!rootNode) { throw new Error(未找到有效页面节点请检查 fileKey 和 nodeId); } return this.extractDashboardIR(rootNode); } /** * 递归遍历节点树提取 Dashboard 结构 */ private extractDashboardIR(node: Recordstring, unknown): DashboardIR { // 识别布局网格 const layout this.detectLayout(node); // 提取所有卡片节点 const cards this.extractCards(node); // 提取主题信息 const theme this.extractTheme(node); // 计算响应式断点 const responsive this.computeBreakpoints(layout, cards); return { title: (node.name as string) || Dashboard, layout, cards, theme, responsive, }; } /** 识别布局模式12 列网格、8 列网格、瀑布流 */ private detectLayout(node: Recordstring, unknown): DashboardIR[layout] { // 通过分析子节点的排列规律判定布局类型 const children node.children as Recordstring, unknown[] | undefined; if (!children || children.length 0) return grid-12; // 提取子节点的宽度分布 const widths children.map((c) (c.absoluteBoundingBox as { width: number })?.width || 0); // 如果大部分子节点宽度能被 12 整除判定为 grid-12 const grid12Count widths.filter((w) w 0 (960 % Math.round(w / 80)) 2).length; return grid12Count widths.length * 0.6 ? grid-12 : grid-masonry; } /** 提取所有卡片节点识别图表类型 */ private extractCards(node: Recordstring, unknown): ParsedCard[] { const children (node.children || []) as Recordstring, unknown[]; return children .filter((child) { // 过滤节点名包含 chart 或 card 且不是装饰元素 const name (child.name as string || ).toLowerCase(); return name.includes(chart) || name.includes(card) || name.includes(table); }) .map((child) this.parseCardNode(child)) .filter((card): card is ParsedCard card ! null); } /** 解析单个卡片节点 */ private parseCardNode(node: Recordstring, unknown): ParsedCard | null { const name (node.name as string || ).toLowerCase(); const box node.absoluteBoundingBox as { x: number; y: number; width: number; height: number } | undefined; if (!box) return null; // 从节点名推断图表类型 let type: ParsedCard[type] metric-card; if (name.includes(line)) type line-chart; else if (name.includes(bar) || name.includes(柱)) type bar-chart; else if (name.includes(pie) || name.includes(饼)) type pie-chart; else if (name.includes(table) || name.includes(表格)) type data-table; return { id: (node.id as string) || card-${Math.random().toString(36).slice(2, 9)}, type, title: this.extractCardTitle(node), position: { x: box.x, y: box.y, width: box.width, height: box.height }, // 网格跨度按 80px 为一列的基准计算 gridSpan: { cols: Math.round(box.width / 80), rows: Math.max(1, Math.round(box.height / 120)), }, }; } private extractCardTitle(node: Recordstring, unknown): string { const children node.children as Recordstring, unknown[] | undefined; const titleNode children?.find((c) (c.type as string) TEXT); return (titleNode?.characters as string) || (node.name as string) || 未命名卡片; } private extractTheme(node: Recordstring, unknown): DashboardIR[theme] { // 从设计稿中的第一个有背景色的节点提取主题信息 const bg node.backgroundColor as { r: number; g: number; b: number } | undefined; const isLight bg ? (bg.r bg.g bg.b) / 3 0.5 : true; return { colorScheme: isLight ? light : dark, primaryColor: #1677ff, borderRadius: 8, gap: 16, }; } private computeBreakpoints( layout: DashboardIR[layout], cards: ParsedCard[] ): DashboardIR[responsive] { const columns layout grid-12 ? 12 : 8; return [ { name: xl, width: 1920, columns }, { name: lg, width: 1440, columns: Math.min(columns, 10) }, { name: md, width: 1024, columns: Math.min(columns, 8) }, { name: sm, width: 768, columns: 4 }, { name: xs, width: 375, columns: 1 }, ]; } }三、代码生成器与模板匹配中间表示DashboardIR生成后进入代码生成阶段。核心是模板匹配引擎根据每个卡片的图表类型和布局位置从模板库中选择最合适的模板进行实例化。// code-generator.ts — Dashboard 代码生成器 // 用途将 DashboardIR 转化为可运行的 React 组件代码 ECharts 配置 import { DashboardIR, ParsedCard } from ./design-parser; interface GeneratedDashboard { componentCode: string; // React 组件代码 styleCode: string; // CSS Modules 样式代码 chartConfigs: Recordstring, string; // cardId → ECharts option JSON dependencies: string[]; // npm 依赖列表 } export class DashboardGenerator { /** * 根据中间表示生成完整 Dashboard 代码 */ generate(ir: DashboardIR): GeneratedDashboard { // 生成图表配置 const chartConfigs this.generateChartConfigs(ir.cards); // 生成组件代码 const componentCode this.generateComponent(ir, chartConfigs); // 生成样式代码 const styleCode this.generateStyles(ir); // 收集依赖 const dependencies this.collectDependencies(ir.cards); return { componentCode, styleCode, chartConfigs, dependencies }; } /** 为每个卡片生成对应的 ECharts 配置 */ private generateChartConfigs( cards: ParsedCard[] ): Recordstring, string { const configs: Recordstring, string {}; for (const card of cards) { configs[card.id] this.generateSingleChartConfig(card); } return configs; } private generateSingleChartConfig(card: ParsedCard): string { const baseConfig { title: { text: card.title, left: center }, tooltip: { trigger: card.type pie-chart ? item : axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, }; switch (card.type) { case line-chart: return JSON.stringify({ ...baseConfig, xAxis: { type: category, data: [周一, 周二, 周三, 周四, 周五, 周六, 周日] }, yAxis: { type: value }, series: [{ data: [], type: line, smooth: true }], }, null, 2); case bar-chart: return JSON.stringify({ ...baseConfig, xAxis: { type: category, data: [] }, yAxis: { type: value }, series: [{ data: [], type: bar }], }, null, 2); case pie-chart: return JSON.stringify({ ...baseConfig, series: [{ type: pie, radius: [40%, 70%], data: [] }], }, null, 2); case data-table: return JSON.stringify({ type: table, columns: [], dataSource: [] }, null, 2); case metric-card: return JSON.stringify({ type: metric, value: 0, label: card.title }, null, 2); default: return JSON.stringify(baseConfig, null, 2); } } /** 生成 React 组件代码 */ private generateComponent( ir: DashboardIR, chartConfigs: Recordstring, string ): string { const importLines [ import React, { useEffect, useState } from react;, import ReactECharts from echarts-for-react;, import styles from ./dashboard.module.css;, ]; const cardRenderers ir.cards.map((card) { if (card.type data-table) { return div className{styles.card} style{{ gridColumn: \span ${card.gridSpan.cols}\ }} div className{styles.cardHeader} h3${card.title}/h3 /div div className{styles.cardBody} Table columns{columns} dataSource{dataSource} sizesmall / /div /div; } if (card.type metric-card) { return div className{styles.card} style{{ gridColumn: \span ${card.gridSpan.cols}\ }} div className{styles.metricCard} span className{styles.metricLabel}${card.title}/span span className{styles.metricValue}{metricValue}/span /div /div; } return div className{styles.card} style{{ gridColumn: \span ${card.gridSpan.cols}\ }} div className{styles.cardHeader} h3${card.title}/h3 /div div className{styles.cardBody} ReactECharts option{chartConfigs[${card.id}]} style{{ height: ${card.gridSpan.rows * 120}px }} / /div /div; }); const columns ir.layout grid-12 ? 12 : 8; return ${importLines.join(\n)} // 自动生成的 Dashboard 组件 // 生成时间: ${new Date().toISOString()} // 卡片数量: ${ir.cards.length} const Dashboard: React.FC () { const [loading, setLoading] useState(true); useEffect(() { // 数据加载逻辑 fetchDashboardData().finally(() setLoading(false)); }, []); if (loading) { return div className{styles.loading}加载中.../div; } return ( div className{styles.dashboard}>