将 AI 生成的设计稿还原为可交互的大屏代码,是 AI 辅助开发流程的最后一步。
本文基于 gpt-image-2 生成的福建省 GDP 可视化大屏设计稿,完整演示了从素材包到可运行代码的实现过程。

开发流程概述
整个流程分为三步:
- 用 gpt-image-2 生成可视化大屏设计稿
- 将设计稿切成素材包
- 将素材包和三维地图模型塞进代码,让大屏真正动起来
本文聚焦第三步——代码还原。
技术栈
项目使用的技术栈:
- React 19 + Vite 7 + TypeScript
- 样式:LESS(变量 + BEM),未使用 Tailwind
- 图表:ECharts 6 + echarts-for-react
- 三维:three.js 0.184(原生写法,未使用 r3f)
- 自适应:autofit.js
苏米注:将 AI 设计稿还原为代码的过程中,会遇到不少细节问题。比如卡片素材图片的标题高度不符合预期、图标切割结果不理想等。关键是多与 AI 交互,让它逐步修改直到满意。

最终将项目拆成三块分开处理:页面布局 + 自适应、图表数据、三维场景。三块各自调通后,再拼在一起。
第一块:页面布局 + 自适应
使用 autofit.js 整体缩放
大屏开发最大的痛点是分辨率。设计稿按 1920×1080 绘制,但实际展示设备从 1366 笔记本到 4K 拼接屏都有可能。
两种解决方案:
- 所有尺寸按百分比/vw/vh 编写
- 按 1920×1080 编写,整体做一次 CSS transform: scale 缩放
第二种方案更省心——按设计稿像素编写,最后整体缩放贴满屏幕。autofit.js 就是做这件事的。
在 main.tsx 中初始化一次:
import autofit from "autofit.js";
autofit.init({
dh: 1080, // 设计稿高度
dw: 1920, // 设计稿宽度
el: "body", // 缩放的目标元素
resize: true,
});
注意:API 参数名是 dw/dh/el,不是文档里有时写的 designWidth/designHeight。
布局用 Flex 而非 Grid
虽然很多文章推荐大屏用 CSS Grid,但实际开发中 Flex 更灵活,尤其是列宽不等、卡片数量不等的情况。
项目结构:
对应的 LESS:
.app-main {
flex: 1;
display: flex;
gap: 8px;
min-height: 0;
overflow: hidden;
}
.col-left, .col-right { width: 490px; }
.col-center { flex: 1; }
左右两列定宽 490px,中间 flex:1 留给地图。整体背景用 background-image 铺上大屏底图,卡片叠在上面。
落地细节
- 底色:使用 #061224,与素材包中深色底的卡片无缝贴合,无需再抠边
- 进场动画:列之间分别用 slideInLeft / scaleIn / slideInRight 做错峰淡入,观感比同时出现更舒适
- 字体:中文使用 Microsoft YaHei / PingFang SC 系统字体,数字指定 DIN Alternate / Consolas 等宽字体
第二块:图表
使用 echarts-for-react 封装 ECharts,避免直接操作 DOM。

为什么不用原生 echarts.init()
使用 echarts-for-react 可以避免"ECharts 初始化时 DOM 还没尺寸,图表渲染不出来"的问题。它内部会监听容器尺寸,React 组件挂载后自动 init 并在尺寸变化时 resize。
import ReactECharts from 'echarts-for-react';
配合父容器 flex: 1; min-height: 0,100% 宽高就能自然撑起来。
深色发光风格配置
ECharts 默认是浅色主题,调成大屏风格需要修改以下配置:
// 取消默认白底
backgroundColor: 'transparent',
// 统一文字颜色
textStyle: { color: 'rgba(255,255,255,0.8)' },
// 面积图渐变(上亮下透)
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(79,195,247,0.25)' },
{ offset: 1, color: 'rgba(79,195,247,0.01)' },
])
},
// 网格线压暗 + 虚线
splitLine: {
lineStyle: { color: 'rgba(27,128,255,0.08)', type: 'dashed' }
},
// 隐藏 Y 轴轴线,只保留刻度
yAxis: { axisLine: { show: false }, axisTick: { show: false } },
颜色使用 LESS 变量(@color-cyan #4fc3f7、@color-yellow #ffd740、@color-green #00e676)保持整屏统一。
第三块:三维地图
这部分耗时最长,也最容易踩坑。FujianMap3D.tsx 写了 400 多行,核心逻辑如下:

使用 GLB 而非 GLTF
Blender 导出时选择 .glb(二进制打包),而非 .gltf(JSON + 外部资源)。GLB 加载时一次 HTTP 请求就能拿到模型+纹理,避免处理相对路径问题。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const gltfLoader = new GLTFLoader();
gltfLoader.load('./models/福建省3.glb', (gltf) => {
const model = gltf.scene;
model.scale.set(1.8, 1.8, 1.8);
model.position.set(0.15, 0.5, 0);
scene.add(model);
model.updateMatrixWorld(true); // 后面要取子节点世界坐标,必须先刷一次
});
相机、灯光与色调
大屏地图的视觉关键是斜上方俯视 + 多光源冷色调:
const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100);
camera.position.set(0, 2.17, 1.25); // 微斜俯视
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0); // 透明背景,露出大屏底图
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
灯光共 4 盏:顶光 + 右侧白光 + 左侧蓝光补光 + 正前方蓝光衬光,叠一层淡蓝色环境光。全部使用 DirectionalLight 即可。
OrbitControls 限位
大屏地图允许用户小范围转动,但不能随便翻转。OrbitControls 必须限制范围:
controls.enablePan = false;
controls.minDistance = 2.5;
controls.maxDistance = 3;
controls.minPolarAngle = controls.maxPolarAngle = Math.PI / 6; // 固定 30° 俯视
controls.minAzimuthAngle = -Math.PI / 9;
controls.maxAzimuthAngle = -Math.PI / 18;
Bloom 与透明背景的兼容问题
UnrealBloomPass 默认会把整个画面(包含背景)一起处理,导致透明底被涂成黑色,大屏底图被完全盖住。
解决方法是修改 bloomPass 内部两个材质的混合模式,让它只叠加 RGB、不动 alpha:
const bp = bloomPass as any;
bp._basic.transparent = true;
bp._basic.depthTest = false;
bp._basic.depthWrite = false;
bp.blendMaterial.blending = THREE.CustomBlending;
bp.blendMaterial.blendSrc = THREE.SrcAlphaFactor;
bp.blendMaterial.blendDst = THREE.OneFactor;
bp.blendMaterial.blendSrcAlpha = THREE.ZeroFactor;
bp.blendMaterial.blendDstAlpha = THREE.OneFactor;
bp.blendMaterial.transparent = true;
Bloom 参数保持温和:strength=0.3, radius=0.2, threshold=0.2,过高会糊成一片。
城市点位:精灵图 + HTML 标签
Blender 中给每个地市占位节点命名(福州、厦门…)。代码中遍历模型,隐藏原节点,在其世界坐标上放置精灵图(THREE.Sprite),同时创建 HTML 元素作为标签层。

model.traverse((obj) => {
const cityName = matchCityName(obj.name);
if (!cityName) return;
const worldPos = new THREE.Vector3();
obj.getWorldPosition(worldPos);
obj.visible = false;
const sprite = new THREE.Sprite(spriteMaterial.clone());
sprite.position.copy(worldPos);
sprite.scale.set(0.06, 0.06, 0.06);
sprite.userData = { cityName, isCityMarker: true };
scene.add(sprite);
});
HTML 标签每帧根据摄像机投影更新屏幕坐标。文字使用 HTML 而非 Canvas 纹理的好处是可以直接用 CSS 的 text-shadow 做发光,像素永远清晰。
点击城市弹出详情
使用 Raycaster 检测精灵图点击:
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(citySprites);
if (intersects.length > 0) {
const cityName = intersects[0].object.userData.cityName;
setSelectedCity(cityName);
setCardPos({ x: event.clientX - rect.left, y: event.clientY - rect.top });
}
弹窗是绝对定位的 React 组件,而非画在 canvas 中。这样样式调整自由,还能直接用 backdrop-filter: blur 做毛玻璃效果。

出场动画
使用 easeOutBack 缓动做弹性入场:模型从 scale 0 弹到 1.8,精灵图延迟 0.2s 出现,HTML 标签延迟 0.4s 淡入,层次感明显。
autofit 缩放下的 DPR 处理
这是一个隐蔽的坑:autofit.js 用 CSS transform: scale 缩放整个 body,此时 window.devicePixelRatio 和 WebGL 渲染缓冲区的实际像素不一致,直接用 renderer.setSize 会导致画面模糊。
正确做法是自己计算渲染尺寸:
const handleResize = () => {
const rect = container.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio, 2);
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
renderer.setDrawingBufferSize(
Math.round(rect.width * dpr),
Math.round(rect.height * dpr),
dpr
);
composer.setSize(Math.round(rect.width * dpr), Math.round(rect.height * dpr));
};
setDrawingBufferSize 是比 setSize 更底层的 API,不会改变 canvas 的 CSS 尺寸——这正是 autofit 场景下需要的。
整合后的问题
三块单独调通后,放入同一个 App.tsx 中整体运行,遇到的问题:
- 3D 画面背景变黑:Bloom 的 alpha 通道未处理
- 标签跟精灵图错位:模型加了 scale/position 后未调用 updateMatrixWorld(true)
- autofit 缩放后三维画面变糊:DPR 问题,用 setDrawingBufferSize 解决
- 图表高度为 0:父容器需加 min-height: 0,否则 flex 子项不会收缩
还原度对比


整体还原度约八九成。差距主要在:
- 发光边缘细节:设计稿中的光效是 AI + PS 调出的,代码 Bloom 是全局均匀的,无法做到那么精准
- 三维地图纹理质感:GPT 生成的贴图比纯色好,但与设计稿对比仍略粗糙
- 部分图标未完全还原:重复性操作较多
但实际使用已足够:数据可接入、图表可联动、地图可点击弹框,全链路可交互,不是死图。
全流程总结
从 AI 设计稿到可交互大屏,经过的链路:
gpt-image-2 → 切素材包 → Blender 建模导 GLB → React + Vite 工程 → ECharts 配图表 → three.js 做三维 → autofit 做自适应
每一步都有独立的坑,但每一步的坑都是可复用的沉淀。
下次做大屏时的优化方向:
- 制作工程提示词模板
- Blender 灯光设置存为预设
- three.js 的 Bloom alpha patch、DPR resize、精灵图+HTML 标签等套路可直接复用
下一步:尝试带地市边界高亮的版本——每个地级市是独立 mesh,悬停整块亮起、点击弹数据卡。省级轮廓数据 DataV 上有现成的 geojson,流程为:geojson → SVG → Blender 拉伸 → 每个 mesh 单独命名。
苏米注:AI 辅助开发的核心价值不在于一次性生成完美结果,而在于快速迭代和可复用沉淀。将 AI 生成的设计稿还原为代码的过程中,虽然会有细节偏差,但全链路可交互的大屏已经具备实际使用价值。