拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ECharts自定义底图实战:SVG转GeoJSON实现大屏可视化

ECharts自定义底图实战:SVG转GeoJSON实现大屏可视化

做可视化大屏这几年,我打交道最多的组合就是 ECharts 加地图系列。大部分需求都逃不过一个诉求:底图得是我们自己的图纸,不是标准行政区划。工业园区、仓储库区、商场楼层、机房平面、地下管廊、停车场布局,这类场景用 ECharts 自带的中国地图或者省市 GeoJSON 是完全对不上号的。我最早的做法是把甲方给的 CAD 图导出 PNG,然后用 graphic 组件铺一张图片,再把散点算好像素坐标硬怼上去。小屏能糊弄,一上 4K 大屏就露馅——图片一放大边缘发虚,缩放拖拽时点位和底图还会飘。后来改用 SVG 当底图,才算把这条链路真正跑顺。这篇就把我踩过的坑、验证过的方案、以及可以直接抄的参数配置,一次讲清楚。

数据可视化这个圈子里,大家聊 ECharts 地图系列时默认谈的是 GeoJSON,但很多时候你手里根本没有 GeoJSON,只有一张设计稿、一张 CAD、一个 AI 文件。SVG恰好是这些工具都能导出的通用矢量格式,把它变成 ECharts 能吃的底图,中间那条路就是本文要讲的。它适合做园区级、楼层级、设备级这种自定义空间的底图绘制,不适合需要真实经纬度做地理分析的场景,这点我会在正文里反复强调,免得你选错方向。

1. 先搞清楚 ECharts 地图系列的底图是怎么来的

1.1 默认地图数据为什么经常不够用

ECharts 的 map 系列本质上是把一份 GeoJSON 注册成一张"地图",然后 series 或 geo 组件按 map 名字去取这份数据来渲染。早期 ECharts 把世界、中国、各省的 GeoJSON 直接打包进主包,后来因为体积和边界数据更新的问题,官方把这些地图数据从包里拆了出去,现在你必须自己准备 GeOJSON 再调echarts.registerMap注册。这一步很多人第一次做地图时都会栽:代码里写map: 'china'结果页面一片空白,控制台还看不到明显报错,因为数据根本没注册。

即便你老老实实引入了官方省份数据,在真实项目里还是不够用。我总结过三类高频问题。第一类是行政边界和时间对不上,某些区域做过调整,社区里的 GeoJSON 更新滞后,画出来的边界和甲方要求的对不上,返工极其难受。第二类是精度过剩,一份省市级 GeoJSON 动辄几百 KB 到几 MB,大屏首屏加载被它拖累,而甲方要展示的只是十几个点位,用不上那么细的海岸线。第三类也是最要命的——需求压根不是行政区划,而是园区内部的功能分区、厂房轮廓、道路、绿化带,这些东西在标准 GeoJSON 里查不到,也不该去查。

1.2 SVG 当底图,到底补上了哪块短板

SVG 是矢量图形格式,本质是用 XML 描述线条、路径、填充色。它能补上的短板其实非常直接。首先是来源自由,AI、Figma、Sketch、CorelDRAW、甚至 CAD 转出的图,都能存成 SVG,设计同学给你什么你就用什么,不用求着 GIS 同事帮你做地理配准。其次是矢量特性,路径是数学描述,放大到 4K、8K 都不糊,而 PNG 底图在 2 倍变焦后就明显发虚。第三是文件体积,一张园区线稿 SVG 通常只有几十到几百 KB,比动辄几 MB 的精细 GeoJSON 友好得多。第四是样式可控,你可以直接改描边颜色、填充透明度来贴合大屏主题,不需要重新导图。

更关键的一点是,SVG 的 path 数据和你想要的"区域"概念天然契合。园区里的 A 栋厂房就是一段闭合路径,停车场就是另一个闭合路径,把它们转成 GeoJSON 的 Polygon 之后,ECharts 就能像处理省份一样处理这些区域——能 hover、能点选、能根据数据填充不同颜色。这是用图片当底图完全做不到的。

1.3 什么场景该上 SVG,什么场景别硬上

判断标准其实就一条:你到底需不需要真实的地理坐标。如果你要做的是区域热力、设备告警、人流统计、管线流向这类"相对位置"表达,SVG 底图完全够用,而且比 GeoJSON 更贴合业务。我做过一个地下管廊项目,管廊走向是设计院出的图纸,根本没有经纬度数据,用 SVG 转 GeoJSON 之后,每个舱段的温度、湿度、告警状态都能挂到对应区域上,效果比硬套地图好得多。

反过来,如果你要做的是真正的 GIS 分析——两点间实际距离计算、缓冲区分析、路径规划、跨区域的地理聚合——那 SVG 底图帮不了你,因为它的坐标是画布坐标,不是经纬度。这种情况下老老实实上 ArcGIS 或者带真实坐标的 GeoJSON。另外,如果项目要求底图带丰富的建筑纹理、卫星影像、地形晕渲,SVG 也不合适,那属于栅格底图的领域。我见过有人硬要用 SVG 画三维建筑,最后得到一个极其复杂的平面图,得不偿失。

2. SVG 底图接入 ECharts 的三条技术路线

2.1 路线一:SVG 转 GeoJSON,走标准 registerMap 流程

这条路线是最"正宗"的,也是我最终在项目里落地的方案。核心思路是把 SVG 里每个代表区域的闭合 path,解析成一系列坐标点,组装成 GeoJSON 的 Polygon 或者 MultiPolygon,然后echarts.registerMap注册成一张自定义地图,后面所有 map series、geo 组件的用法都和标准地图一模一样。它的好处是能用上 ECharts 地图系列的全部能力:区域填色、hover 高亮、label、tooltip、区域与数据绑定、visualMap 映射,甚至连select选中态都能直接用。

代价是转换这一步有工作量。你需要写一个 path 解析器,把d属性里的指令翻译成点序列。听起来吓人,其实核心逻辑不到一百行代码,后面第 4 章我会把脚本贴出来。转换过程里有两个细节必须处理:一是 SVG 的坐标系 y 轴向下,而 ECharts 地图渲染时 y 轴向上,需要做翻转;二是每个闭合路径要确保首尾点一致,否则填充可能出问题。

2.2 路线二:用 graphic 组件把 SVG 当图形层叠加

如果你只是想在一张底图上放点位,不需要区域级别的交互,那 graphic 组件更省事。ECharts 5 的 graphic 支持type: 'image',可以直接加载一张 SVG 图片作为背景层,然后用type: 'group'组织散点、标签、连接线。这种方式的优势是 SVG 原有样式完完整整保留,设计稿什么样,大屏上就什么样,不需要任何数据转换。

但它的问题也很明显。散点坐标你得自己算,而且要在 SVG 的 viewBox 坐标系和 ECharts 画布坐标系之间做映射,一旦涉及 zoom 和 roam,坐标换算会变得很折磨。更麻烦的是,graphic 层和 map 层不是一个坐标系,你想让某个点和某个区域联动,得手动维护两套坐标。我早期做过一版,后来因为 roam 拖拽后点位跟不上底图,果断放弃了。

2.3 路线三:图片或 background 兜底

最省事的做法是把 SVG 转成高分辨率 PNG,通过 series 的type: 'map'配合map: 'custom'不太行,更常见的还是 geo 组件的 background 或者直接一个绝对定位的 img 放在图表容器下层。这种方式胜在快,半小时能出效果,适合原型演示。但它的缺点同样致命:图片不能随容器缩放保持清晰,点位坐标是写死的像素,换个分辨率就全乱,而且完全没有区域交互。

下面这张表是我对三条路线的横向对比,你可以按项目实际情况对号入座。

对比维度SVG 转 GeoJSONgraphic 叠加图片兜底
实现难度中,需要转换脚本中,需坐标映射低,直接铺图
区域级交互完整支持不支持不支持
缩放清晰度矢量,无损矢量,无损依赖图片分辨率
与数据绑定直接用 map series手动维护坐标手动维护坐标
适配 roam 缩放原生支持需自行换算基本不可用
适用场景园区、楼层、管廊简单点位展示快速原型

3. 手工做一张能用的 SVG 底图

3.1 素材从哪来,别一上来就自己画

做底图最忌讳的就是从零开始画。园区有规划图,楼层有平面图,机房有布线图,这些都是现成素材。我的常规流程是:先找甲方要原始图纸,CAD 的 dwg 最好,设计稿 ai、figma 也行,实在没有就上百度地图截一张做参考描线。拿到 dwg 之后,用 Illustrator 或者 Inkscape 打开,删掉标注、尺寸线、图框这些不需要的元素,只保留区域轮廓,然后导出 SVG。Inkscape 是免费的,处理 CAD 转出的矢量图非常顺手,我一般用它做前期清理。

如果连原始图纸都没有,只有一张 JPG 效果图,那就用 Inkscape 的"位图描摹"功能,把图片转成矢量路径。描摹出来的路径会比较碎,需要手动用节点工具合并简化,只留下你要做交互的那几个大区域。这个过程有点耐心活,但比硬啃 CAD 数据快多了。

3.2 viewBox 和坐标系必须先对齐

SVG 的viewBox属性决定了它的内部坐标系,格式是viewBox="minX minY width height"。这个值直接决定了你后续转换出来的坐标范围,必须一开始就理清楚。我遇到过最常见的问题,是设计同学导出 SVG 时 viewBox 起点不是 0 0,比如viewBox="120 80 800 600",结果转换时没有减掉偏移量,地图整体偏出画布。所以拿到 SVG 第一件事,就是用文本编辑器打开,看根标签上的 width、height 和 viewBox。

统一的做法是把 viewBox 规整到0 0 width height,这样坐标原点在左上角,转换逻辑最干净。规整的方法很简单,用 Inkscape 打开后"文档属性"里把页面尺寸贴合绘图内容,重新导出即可。另外要注意 SVG 里的图层(g 标签)可能带transform属性,比如translate、scale,这些变换会影响子路径的实际坐标。要么在导出前把变换"应用"掉(Inkscape 里有"应用变换"操作),要么在解析脚本里做矩阵运算,显然后者麻烦,我更推荐前者。

3.3 给底图瘦身,把节点数压下来

从 CAD 导出的 SVG 路径往往节点极其密集,一条厂房轮廓可能有上千个点。这种数据直接转 GeoJSON,体积会膨胀得很难看,渲染时也可能卡。瘦身的思路有三步。第一步是合并同图层元素,把同一种颜色、同一类填充的区域合成一个 group,便于批量处理。第二步是简化路径,Inkscape 的"路径简化"或者在线工具都能按容差抽稀节点,一般把容差设到 1 到 3 个像素单位,肉眼几乎看不出差别,节点能砍掉一大半。第三步是去掉冗余属性,SVG 里那些style、class、id你如果用不上就删掉,导出前用 SVGO 压一遍,体积还能再降。

这里有个经验值供参考:一张 A1 幅面的园区平面图,清理前 SVG 大概 1.5 MB,节点总数几万个;简化加压缩之后能压到 100 到 200 KB,节点控制在几千个以内,ECharts 渲染毫无压力。这个体量放进大屏首屏,完全不用担心加载问题。

3.4 导出前的自检清单

导出之前我会过一遍这几项,任何一项不过关,后面转换都要返工。坐标原点是不是 0 0;所有需要交互的区域是不是独立闭合路径;路径有没有应用掉 transform;填充色是不是统一,避免同区域出现多个碎块;文本元素是不是单独分离出来,别混在区域路径里;最后确认路径没有用到 SVG 的圆弧A指令,如果有,要么转成贝塞尔,要么在解析脚本里做处理。

注意:SVG 的path里如果混用了A(椭圆弧)指令,简单解析器很容易算错。稳妥做法是在矢量软件里把圆弧转成曲线,Inkscape 的"路径转曲线"能批量处理。

4. SVG 转 GeoJSON 的完整实操

4.1 先理解 GeoJSON 的坐标结构

ECharts 能识别的 GeoJSON 是标准格式,最外层是FeatureCollection,每个Feature代表一个区域,properties.name是区域名,geometry描述形状。对于面状区域,geometry 的 type 是Polygon,coordinates 是一个三维数组:第一层是环的集合,第二层是外环的点序列,第三层是每个点的[x, y]。如果区域带孔洞,外环后面跟内环。这里的坐标是二维的,不需要真实经纬度,ECharts 只要求坐标系一致即可。

结构大致长这样:

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "A栋厂房" }, "geometry": { "type": "Polygon", "coordinates": [ [[0, 0], [200, 0], [200, 120], [0, 120], [0, 0]] ] } } ] }

所以转换脚本要做的事情很清楚:从 SVG 的每个 path 里提取点序列,塞进 coordinates,把 path 的 id 或者自定义属性作为 name。

4.2 path 的 d 指令怎么读

SVG path 的d属性就是一串绘图指令,常见的命令有这些。大写表示绝对坐标,小写表示相对上一个点的偏移。M/m是移动到起点,L/l是画直线,H/h和V/v是水平、垂直直线,C/c是三次贝塞尔曲线,Q/q是二次贝塞尔,Z/z是闭合路径。我们做底图转换,多数情况下只需要处理 M、L、H、V、Z,因为设计图里的轮廓通常已经转成了折线。如果遇到 C、Q,就是把控制点也采样成点,密集采样能逼近曲线,视觉上几乎无差别。

解析的关键点是把相对坐标还原成绝对坐标,用一个当前点游标一路累加。同时注意隐式重复,比如M0,0 10,10 20,20后面的坐标对是连续 L 的简写,很多设计软件导出的路径都这德行,不处理会漏点。

4.3 一个够用的转换脚本

下面这个 Python 脚本我用了很久,处理标准折线路径足够。它对 M、L、H、V、Z 做完整支持,遇到 C、Q 用密集采样近似,输出就是可以直接给 ECharts 用的 GeoJSON。

import re import json TOKEN_RE = re.compile(r'([MLHVCSQTAZmlhvcsqtaz])|(-?\d*\.?\d+(?:[eE][-+]?\d+)?)') def parse_path(d, curve_steps=12): tokens = TOKEN_RE.findall(d) flat = [t[0] if t[0] else t[1] for t in tokens] points = [] cur = [0.0, 0.0] start = [0.0, 0.0] i = 0 cmd = None while i < len(flat): tok = flat[i] if re.match(r'[A-Za-z]', tok): cmd = tok i += 1 if cmd in 'Zz': if points and points[0] != points[-1]: points.append(points[0][:]) cur = start[:] continue # 读取坐标,根据命令决定参数个数 def num(): nonlocal i v = float(flat[i]); i += 1; return v if cmd in 'Mm': x, y = num(), num() if cmd == 'm': x += cur[0]; y += cur[1] cur = [x, y]; start = [x, y] points.append([round(x, 3), round(y, 3)]) elif cmd in 'Ll': x, y = num(), num() if cmd == 'l': x += cur[0]; y += cur[1] cur = [x, y] points.append([round(x, 3), round(y, 3)]) elif cmd in 'Hh': x = num() if cmd == 'h': x += cur[0] cur = [x, cur[1]] points.append([round(x, 3), round(cur[1], 3)]) elif cmd in 'Vv': y = num() if cmd == 'v': y += cur[1] cur = [cur[0], y] points.append([round(cur[0], 3), round(y, 3)]) elif cmd in 'Cc': p = [num() for _ in range(6)] c1 = [cur[0] + p[0], cur[1] + p[1]] if cmd == 'c' else p[0:2] c2 = [cur[0] + p[2], cur[1] + p[3]] if cmd == 'c' else p[2:4] end = [cur[0] + p[4], cur[1] + p[5]] if cmd == 'c' else p[4:6] for s in range(1, curve_steps + 1): t = s / curve_steps mt = 1 - t x = mt**3*cur[0] + 3*mt*mt*t*c1[0] + 3*mt*t*t*c2[0] + t**3*end[0] y = mt**3*cur[1] + 3*mt*mt*t*c1[1] + 3*mt*t*t*c2[1] + t**3*end[1] points.append([round(x, 3), round(y, 3)]) cur = end else: i += 1 return points def svg_to_geojson(paths): features = [] for name, d, height in paths: pts = parse_path(d) if len(pts) < 3: continue # SVG y 轴向下,翻转以适配 ECharts flipped = [[p[0], round(height - p[1], 3)] for p in pts] if flipped[0] != flipped[-1]: flipped.append(flipped[0][:]) features.append({ "type": "Feature", "properties": {"name": name}, "geometry": {"type": "Polygon", "coordinates": [flipped]} }) return {"type": "FeatureCollection", "features": features}

调用的时候,把从 SVG 里提取出的<path>的id和d属性成对喂进去,再传一个 viewBox 的高度做翻转基准,输出的 JSON 直接写文件即可。脚本用了非局部变量递增读取 token,处理隐式重复坐标靠的是循环里遇到数字就按当前命令继续消费,这点是解析器必须做对的地方,否则节点会丢。

4.4 注册地图并接上数据

拿到 GeoJSON 之后,前端这边非常轻。先注册,再配置 geo 和 series。下面这段配置是我大屏项目里的基础骨架,可以直接改成你自己的配色。

import * as echarts from 'echarts'; import parkMap from './park-map.json'; echarts.registerMap('park', parkMap); const chart = echarts.init(document.getElementById('map')); chart.setOption({ geo: { map: 'park', roam: true, aspectScale: 1, layoutCenter: ['50%', '50%'], layoutSize: '92%', itemStyle: { areaColor: '#0b2440', borderColor: '#3aa0ff', borderWidth: 1 }, emphasis: { itemStyle: { areaColor: '#164a7d' }, label: { show: true, color: '#ffffff', fontSize: 14 } } }, series: [ { name: '区域负荷', type: 'map', geoIndex: 0, data: [ { name: 'A栋厂房', value: 320 }, { name: 'B栋仓库', value: 180 }, { name: 'C区停车场', value: 96 } ] } ], visualMap: { min: 0, max: 400, left: 20, bottom: 20, text: ['高', '低'], calculable: true, inRange: { color: ['#0b2440', '#1c6fb8', '#4fc3ff'] } } });

这段配置里aspectScale: 1是关键,后面第 5 章细说。layoutCenter和layoutSize用来居中撑满容器,比 left、top、width 的组合更稳。

5. 大屏实战中的参数调优

5.1 aspectScale 到底要不要设成 1

ECharts 的aspectScale默认值是 0.75,它的设计初衷是配合地理投影,让地图在屏幕上看起来比例正常。但如果你用的是 SVG 转出来的自定义底图,这个 0.75 反而会把你的图压扁或者拉长,因为你的坐标本来就是按图纸比例 1:1 来的。所以自定义底图场景下,aspectScale直接设成 1,让 x 和 y 等比例缩放,图形才不会变形。我一开始没注意这个参数,园区的方形厂房被压成了扁的,找了好久才定位到它。

5.2 zoom、center 和 layoutCenter 怎么配合

控制地图在容器里的位置有两条路子。一条是用zoom加center,center 是地图坐标系里的一个点,会移到容器正中,zoom 控制缩放倍数。另一条是用layoutCenter加layoutSize,前者是个百分比坐标,表示地图布局盒的中心在容器中的位置,后者是布局盒的尺寸。我的建议是布局用 layoutCenter 和 layoutSize,交互缩放交给 roam 或者 zoom。两套参数混用时容易互相打架,尤其是开了 roam 之后再设 center,用户拖一下就跳回去了,体验很糟。

roam我一般设 true,但对于纯展示大屏会设成'scale',只允许缩放不允许平移,防止观众误触把图拖出可视区。如果底图是横长条或者竖长条,layoutSize 可以设成['90%', '70%']这样的双值,分别控制宽高占比,比等比缩放灵活。

5.3 tooltip 自动换行和 labelLine 的细节

大屏上文字长是常态,tooltip 默认不换行,提示框会横着拉出屏幕外。解决办法是给 tooltip 开自动换行,extraCssText里加white-space: normal和word-break: break-all,再配合 formatter 里手动插<br/>,双保险。下面这个写法我用了好几年,长文案和其他标点混排都不会溢出。

tooltip: { trigger: 'item', confine: true, formatter: function (params) { const v = params.value == null || isNaN(params.value) ? '-' : params.value; return params.name + '<br/>' + '负荷指数:' + v; }, extraCssText: 'white-space: normal; word-break: break-all; max-width: 260px; line-height: 20px;' }

饼图或者地图标签用 labelLine 时,末尾小圆点的偏移是个高频问题。ECharts 5 里 labelLine 的length控制第一段引线长度,length2控制第二段长度,smooth控制是否平滑。小圆点其实跟引线末端位置绑定,你调 length2 就能把小圆点往里收或者往外推。如果标签太长顶到边缘,用label.alignTo: 'edge'让标签自动贴边,配合edgeDistance留出安全距离,远比手动调数值靠谱。

5.4 自适应和 rem 换算失效的问题

大屏适配绕不开 rem,用 postcss-pxtorem 把 CSS 里的 px 转成 rem 是常规操作。但很多人发现 ECharts 里的字体、间距怎么都不跟着缩,原因很简单:ECharts 走的是 canvas 渲染,尺寸配置写在 JS 里,postcss 只处理 CSS 文件,管不到 JS 里的 px。所以你得自己写一个换算函数,在 setOption 之前把需要的 px 值转成 rem,或者在窗口 resize 时整体重设 option。

我的做法是封装一个px2rem工具,基准值取设计稿宽度的十分之一,所有传给 ECharts 的 fontSize、symbolSize、labelLine 长度都过一遍这个函数。同时监听 resize 事件,防抖之后调chart.resize(),并重新计算一次 layoutSize 的百分比。这套下来,从 1920 到 4K 分辨率切换,地图和文字基本不会乱。

注意:chart.resize()只重算画布尺寸,不会重新解析 option 里的 px 配置。字体、间距这类需要自己监听 resize 后重新 setOption,否则文字大小不跟随变化。

6. 踩坑记录与常见问题速查

6.1 地图一片空白怎么查

空白是最高频的问题,我总结了排查顺序。先看 registerMap 注册的名字和 geo.map、series.map 里写的名字是否完全一致,大小写、空格都算。再看 GeoJSON 的坐标系是不是正常,如果所有点都是 0 或者天文数字,说明解析时相对坐标没还原。接着看数据里的 name 和 GeoJSON 的 properties.name 是否对得上,地图能显示但区域不填色,基本都是这个原因。最后看一眼容器的宽高,ECharts 初始化时容器如果高度是 0,图上什么都不会画,这是 CSS 没给高度导致的。

6.2 图形变形、错位、翻转的解法

变形通常两个原因。一个是 aspectScale 没设成 1,属于配置问题。另一个是 SVG 的 viewBox 起点不是 0 0,转换时没减偏移,导致整张图位移。翻转问题就是我脚本里处理的那步,SVG 的 y 轴朝下,ECharts 朝上,不做height - y翻转,图形会上下颠倒。还有一种隐蔽的错位是 transform 没应用,g 标签上的translate(100, 50)会让子路径整体偏移,解析时忽略它就全乱套。

6.3 性能卡顿的处理思路

自定义底图卡顿一般三个来源。一是 path 节点太多,几千个点以上的区域 hover 时会明显掉帧,用路径简化抽稀。二是区域数量过多,一张图上几百个可交互区块,ECharts 的事件绑定会吃力,这种要考虑按需渲染或者做区域聚合。三是 roam 缩放时重绘压力大,可以设progressive和progressiveThreshold做渐进渲染,或者在缩放时临时关掉 label。我做过一个上千区块的管廊图,最后是拆成几张小图分层渲染才顺过来。

6.4 常见问题速查表

现象可能原因处理方式
地图完全不显示未注册或 map 名不一致检查 registerMap 与 map 字段
显示但不填色data 的 name 与 GeoJSON 不匹配对齐 properties.name
图形上下颠倒未做 y 轴翻转转换时用 height 减 y
图形被压扁aspectScale 默认 0.75设为 1
整体偏移出画布viewBox 起点非 0 0规整 viewBox 或减偏移
tooltip 横穿屏幕未允许换行extraCssText 加 white-space normal
字号不随屏幕缩放postcss 管不到 JSJS 内自行 px 转 rem
缩放后点位飘移graphic 与 map 坐标系不一致改用 GeoJSON 方案
交互卡顿路径节点或区域过多简化路径、分层、渐进渲染

我在项目里越来越倾向于这样一个组合:SVG 转 GeoJSON 做区域层,点位和流向单独用一个 scatter 或者 lines series 挂到同一个 geo 坐标系上。这样地图的区域交互和点线表达各司其职,改需求时互不影响。转换脚本建议单独建个工具目录存着,下次换项目换个 SVG 改改高度参数就能复用,比每次重新调坐标划算太多。真正让我省心的不是某一个参数,而是把"底图从哪来、怎么转、转到什么程度"这三件事提前想明白,动工的时候才不会来回返工。

返回列表