资讯动态

ECharts实战指南:从基础配置到大屏应用,避开常见坑

发布时间:2026/10/1 1:45:23 来源:尧图企业网站定制
1. 为什么我在项目里绕不开 ECharts上个月接到一个数据可视化大屏的需求甲方要求把业务数据里的订单趋势、地区分布、品类占比全部摆到一张大屏上。刚开始我还想着自己用 SVG 一块块画结果越画越不对劲光是折线图的坐标轴刻度、柱状图的 hover 提示、饼图标签线的对齐就够我折腾一整周。后来果断换回 ECharts半天时间把三种图表全部搭完剩下的时间全花在调样式和对接接口上。这个场景你应该不陌生。ECharts 几乎是目前前端做图表绕不开的库Apache 基金会顶级项目底层基于 Canvas图表类型覆盖折线、柱状、饼图、地图、雷达、桑基、关系图等等交互和动画开箱即用。如果你要处理数据可视化尤其是后台管理系统、数据大屏、报表中心这类需求ECharts 基本是首选。这篇内容我打算换个讲法不按官网文档从 option 的每个字段过一遍而是围绕我在真实项目里经常被问到的几个点展开柱状图、折线图、饼图的配置内核地图数据怎么加载大屏项目里的实战姿势还有几个特别隐蔽的坑比如 pxtorem 对 ECharts 不生效、tooltip 换行、labelLine 小圆点偏移。不管你是刚接触图表的新手还是已经被 ECharts 坑过几次的开发者这篇文章应该都能给你一些能直接抄作业的东西。2. 柱状图、折线图、饼图的配置内核看透 series 和坐标轴的关系很多入门教程都会把柱状图绘制当作第一关确实ECharts 的柱状图是理解整个配置体系的钥匙。但如果你只是照着示例代码粘贴不理解 series、xAxis、yAxis、grid 这几个概念之间怎么配合后面遇到折线图 x 轴刻度错位、饼图 labelLine 偏移这类问题就会完全摸不着头脑。我先用最基础的三类图把这套内核讲透。2.1 先搞懂 series、xAxis、yAxis 和 grid 是怎么协作的一张 ECharts 图表本质上可以拆成两部分坐标系和图形。坐标系由 xAxisx 轴、yAxisy 轴、grid网格区域组成图形则由 series 定义。柱状图和折线图都跑在直角坐标系上饼图则完全不需要坐标系它自己有另一个维度。看一个最基础的柱状图配置const option { grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: [周一, 周二, 周三, 周四, 周五] }, yAxis: { type: value }, series: [{ type: bar, data: [120, 200, 150, 80, 170] }] };这里最关键的一点是xAxis.data和series.data是通过数组下标一一对应的。也就是说x 轴第一个分类对应 series 里第一个数值第二个分类对应第二个数值以此类推。这个对应关系听起来简单却是很多刻度错位问题的根源——当你动态更新数据时如果只改了 series.data 而忘了同步 xAxis.data图表不会报错但显示的内容已经全错了。grid的作用很多人容易忽略。它控制的是坐标系在整个 canvas 里的位置和大小。containLabel: true是我建议每个项目都加上的配置它的含义是让轴刻度标签也包含在 grid 范围内防止左侧的数值标签被截断。如果不加有时候 y 轴的刻度文字会超出容器边界看起来就像被切了一刀。series 才是图表的核心它决定了图形类型和视觉样式。ECharts 支持一个 option 里放多个 series这就实现了组合图。比如一个柱状图加一个折线图只需要在 series 数组里放两个对象一个type: bar一个type: line它们可以共用同一套 xAxis 和 yAxis。做双 y 轴图表时还可以给某个 series 指定单独的yAxisIndex: 1然后在 yAxis 数组里配置第二个轴。理解了这套协作逻辑组合图、双轴图就不在话下了。2.2 折线图 x 轴刻度不齐、刻度拥挤怎么调折线图的 x 轴刻度问题我几乎每隔一段时间就会被问一次。热搜词里也有echarts折线图x轴刻度说明这是高频痛点。常见的有两种表现一是数据点只有几个但 x 轴默认从 0 开始导致折线没有紧贴 y 轴二是时间跨度大刻度标签挤成一团。第一种情况几乎都是boundaryGap在作怪。柱状图的 x 轴分类轴默认boundaryGap: true意思是柱子和 y 轴之间留有空隙看起来更美观。但折线图通常希望第一个点和 y 轴对齐或者至少靠近 y 轴这时候需要手动设置xAxis: { type: category, boundaryGap: false, data: [周一, 周二, 周三, 周四, 周五] }boundaryGap: false会让第一个数据点落在 x 轴起点位置折线从边缘开始延伸视觉上更贴合趋势的感觉。第二种刻度拥挤的问题发生在数据量大的时候。比如你展示一年 365 天的访问量ECharts 会尝试把所有日期都显示在 x 轴上结果标签叠在一起变成一坨黑色。解决办法有几种xAxis: { type: category, data: dates, axisLabel: { interval: 13, // 每隔 13 个刻度显示一个标签 rotate: 30, // 或者旋转 30 度 hideOverlap: true // 自动隐藏重叠标签 } }interval可以填数字表示每隔几个标签显示一个也可以填一个函数根据索引决定是否显示。rotate适合标签文字较长的情况旋转后能避免挤压。hideOverlap是 ECharts 5 提供的自动隐藏能力标签重叠时自动丢掉一部分适合数据量不确定的动态场景。我个人的习惯是数据量在 10 个以内不做处理超过 20 个优先用interval配合hideOverlap超过 100 个就不建议用 category 轴展示全部了考虑聚合或 sampling。折线图还有一个容易踩的坑是smooth: true之后数据极小值附近会出现过冲曲线看起来超出了实际数值范围。这不是 bug是贝塞尔曲线的正常表现如果业务不允许这种情况把smooth改成false或者用smoothMonotone: x限制单调性。2.3 饼图 labelLine 末尾小圆点偏移的问题根因在 label 和 labelLine 的配合饼图的坑和柱状图、折线图完全不同它没有坐标轴全靠series里的label和labelLine控制标签。热搜词里echarts 饼图 labelline 末尾小圆点偏移我猜大概率是下面这个场景饼图的引导线末端带一个小圆点但圆点和引导线不居中对齐或者引导线和文字之间的距离忽大忽小。其实偏移这个说法根源通常不是小圆点本身而是 label 的定位方式。ECharts 饼图的 label 有几种位置outside外侧、inside内部、center中心。当 label 在外部时labelLine 负责连接扇区和文字。文字默认是左对齐或右对齐取决于它在饼图的哪一侧。如果你的 label 内容换行了或者 label 里设置了padding、lineHeight文字区域和 labelLine 的锚点就对不上了视觉上就像小圆点偏移了。我常用的处理方案是这样series: [{ type: pie, data: pieData, label: { alignTo: labelLine, formatter: function (params) { return params.name \n params.value; }, lineHeight: 20 }, labelLine: { length: 12, length2: 12, smooth: true } }]alignTo: labelLine会把 label 整体对齐到引导线末端这样多行文本会和 labelLine 保持整齐的边距。lineHeight则控制多行文字的行间距避免两行文字叠在一起。length是引导线从扇区延伸出来的长度length2是引导线末端的水平段长度调这两个值就能控制文字与饼图之间的距离。如果你想要更精细的控制可以用rich富文本。比如想让名称和数值字体大小不一样label: { formatter: {name|{b}}\n{value|{c}}, rich: { name: { fontSize: 14, color: #333, lineHeight: 24 }, value: { fontSize: 18, color: #ff6600, fontWeight: bold, lineHeight: 24 } } }富文本是 ECharts 标签系统里非常强大但其实很多教程没讲透的功能。它不仅能控制样式还能在标签里混排小图标、换行、对齐。做饼图、雷达图、桑基图的时候学会用rich能省掉大量 CSS 布局的麻烦。2.4 tooltip 自动换行和内容格式化的实战方案tooltip 的换行问题我见过不止一个同事在处理多指标展示时踩坑。默认情况下tooltip 的内容如果太长会一直横向延伸不会自动换行。热搜词echarts tooltip自动换行背后的真实需求一般是我要在 tooltip 里展示多个字段每个字段一行字段名和值最好对齐。最直接的换行方式是在formatter函数里拼接\ntooltip: { trigger: axis, formatter: function (params) { const item params[0]; return item.name br/ 访问量 item.value[1] br/ 环比增长 item.data.rate %; } }注意这里\n和br/是等价的ECharts 内部会把换行符转成br/。如果你的需求是多指标、多系列还可以利用 formatter 的第二个参数手动拼表格tooltip: { trigger: axis, formatter: function (params, ticket, callback) { let html params[0].axisValue; params.forEach(function (item) { html div item.marker item.seriesName item.value /div; }); return html; } }item.marker会自动生成一个对应系列颜色的小圆点配合自定义 HTML 结构tooltip 就能展示出带颜色的多行内容。如果你想用 CSS 控制换行和间距还可以在 formatter 里返回带div和span的 HTML然后用tooltip.extraCssText属性注入自定义样式tooltip: { extraCssText: max-width: 300px; white-space: normal; word-break: break-all; }这是真正能解决 tooltip 内容过长问题的配置。把white-space: normal加上去内容到宽度上限就会自动折行不用手动拼br/。遇到长文案或者动态拼接的用户输入内容时这招尤其管用。3. 地图数据可视化别再以为地图是 ECharts 自带的热搜词里有echarts中国地图我猜很多人是从这里开始接触 ECharts 地图的。有个非常常见的误解以为 ECharts 安装完地图就自动能用了series里填type: map就行。结果一运行报错Component series.map not exists或者Map china not exists然后就开始怀疑人生。3.1 地图数据其实是一份 GeoJSON需要手动注册ECharts 本身只提供了地图的渲染引擎并不自带任何国家或省市的地图数据。你要展示地图必须先拿到对应区域的地理数据然后调用echarts.registerMap把它注册进去最后在 series 里通过map属性引用注册的名称。地理数据的标准格式是 GeoJSON简单理解就是一份用坐标点描述多边形边界的 JSON 文件。每个区域有type、properties、geometry等字段properties里通常包含区域名称也就是你展示时用来匹配数据用的 key。获取 GeoJSON 的常见途径是公开的地理数据仓库或者直接从别人封装好的 npm 包下载。拿到之后代码长这样import * as echarts from echarts; import chinaJson from ./china.json; echarts.registerMap(china, chinaJson); const chart echarts.init(document.getElementById(mapContainer)); chart.setOption({ series: [{ type: map, map: china, roam: true, label: { show: true } }] });registerMap接收两个参数第一个是你给地图起的名字第二个是 GeoJSON 数据。这个名字是全局注册的后续所有图表都可以通过map: china引用。roam: true允许用户缩放和平移地图做展示大屏时如果不想让用户随便拖可以关掉。这里有个细节值得多说一句地图数据的体积通常不小一个中国地图的 GeoJSON 动辄几百 KB。如果项目首屏加载对性能敏感建议按需加载比如进入某个页面时才import对应的 JSON或者放到静态资源目录用 Ajax 拉取不要一股脑打进主 bundle。3.2 地图上的数据映射与视觉映射地图数据注册好了接下来要做的是把业务数据映射到地图上。最常见的需求是不同省份显示不同颜色数值越大颜色越深。这就要用到visualMap组件const option { visualMap: { min: 0, max: 1000, left: left, top: bottom, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] } }, series: [{ type: map, map: china, data: [ { name: 北京, value: 100 }, { name: 上海, value: 320 }, { name: 广东, value: 800 } ] }] };visualMap的min和max是映射的数值范围inRange.color是一个颜色渐变数组ECharts 会根据 value 在这个范围内自动插值。series.data 里每个对象的name必须和 GeoJSON 的properties.name完全一致否则匹配不上那个区域就会显示为透明或者无颜色。这块的名字匹配是新手最容易翻车的地方——当你看地图发现某块区域不上色九成是名称对不上。排查时可以把 GeoJSON 打开直接搜一下里面实际用的名称是什么。如果只想给某个区域高亮不需要颜色渐变可以给 series 添加itemStyle配合select状态。或者用label.show在区域上显示名称配合emphasis配置鼠标悬停效果series: [{ type: map, map: china, label: { show: true, fontSize: 10 }, itemStyle: { areaColor: #aaccff, borderColor: #fff }, emphasis: { label: { show: true, fontWeight: bold }, itemStyle: { areaColor: #ff9944 } } }]3.3 地图与散点图的结合技巧做地区分析时光看省份颜色还不够往往还希望在地图上叠加具体城市的位置点比如客户分布、门店分布。这时候通用的思路是底层用 map 系列展示区域底图再叠加一个scatter系列用经纬度坐标定位series: [ { type: map, map: china, roam: true }, { type: effectScatter, coordinateSystem: geo, data: [ { name: 北京, value: [116.4, 39.9, 100] }, { name: 上海, value: [121.47, 31.23, 88] } ], symbolSize: function (val) { return val[2] / 10; } } ]注意这里的coordinateSystem: geo是关键它告诉 ECharts 这个散点系列的坐标系是地理坐标系而不是直角坐标系。value 数组的前两位必须是经纬度第三位可以作为散点大小或其他维度的数值。effectScatter是带涟漪动画效果的散点适合做数据大屏上的重点标记视觉效果比普通 scatter 好不少。有一点需要提醒map 系列虽然可以展示数据但它本质上是地理坐标系统如果要在地图上叠加其他地理元素最好统一用geo组件来承载底图series 里的 map 类型做数据展示具体取舍看你项目里地图和数据的使用频率。如果只是画一张静态的地图看得分用 series.map 就够了如果地图上还要叠加飞线、散点、涟漪建议把geo单独拎出来配置这样 series 之间互相独立不容易因为地图数据设置互相干扰。4. 把 ECharts 塞进真实项目大屏可视化和数据对接ECharts 的示例代码看起来都很简单但真正把它放进项目里尤其是数据可视化大屏考验的就不再是 option 本身而是初始化时机、数据更新策略、自适应方案、组件生命周期这一整套工程问题。基于我实际做过的几个大屏项目把关键的点串一遍。4.1 大屏项目的整体布局思路数据可视化大屏通常是一整块填满浏览器窗口的页面上面分布多个图表。布局上最省事的方案是 CSS Grid 或 Flex 弹性布局把屏幕分成几个区域每个区域放一个图表容器。容器的宽高不能写成固定像素否则不同分辨率下会错位建议用百分比或者vh、vw单位。但这里有个隐藏问题如果大屏需要适配多种分辨率屏幕简单设置百分比宽高在小屏设备上图表文字和间距可能会显得很大。我常用的一种思路是设计稿固定为 1920x1080页面根节点根据实际视口尺寸做transform: scale()等比缩放让整块大屏保持设计稿比例。图表容器内部再用百分比布局ECharts 初始化时读取到的容器宽高就是缩放后的 CSS 像素宽度canvas 会自动适配。举个例子.dashboard { width: 1920px; height: 1080px; transform-origin: left top; }function scaleDashboard() { const el document.querySelector(.dashboard); const scaleX window.innerWidth / 1920; const scaleY window.innerHeight / 1080; const scale Math.min(scaleX, scaleY); el.style.transform scale( scale ); } window.addEventListener(resize, scaleDashboard); scaleDashboard();这个方案做数据大屏时非常稳因为它从根源上保证布局比例一致ECharts 的容器宽高始终是设计稿的尺寸不会出现某个图表文字特别大、某个图表特别小的情况。等比例缩放后四周会留出空白可以把背景色设成深色主题视觉上几乎看不出瑕疵。4.2 原生 JS、jQuery、Ajax 和 ECharts 的组合玩法虽然现在很多项目都是 Vue 或 React但依然有大量旧系统和外包项目在用原生 JS、jQuery需要手动操作 DOM。我处理过一个纯 jQuery 项目要求把后端接口的数据用 ECharts 展示出来思路其实很朴素ECharts 初始化之后用 jQuery 的$.ajax请求数据拿到数据后调用setOption更新图表。核心代码如下const chart echarts.init(document.getElementById(chart)); $.ajax({ url: /api/trend, method: GET, dataType: json, success: function (res) { if (res.code ! 0) { // 接口异常处理 return; } const dates res.data.map(function (item) { return item.date; }); const values res.data.map(function (item) { return item.value; }); chart.setOption({ xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ type: line, data: values }] }); } });如果接口是类似每 5 秒刷新一次的轮询场景可以把 Ajax 请求封装成一个fetchData函数用setInterval定时调用。这里有一个非常重要的细节再次setOption之前是否需要先chart.clear()我的经验是不需要。ECharts 的setOption默认是增量合并的只要你传入的 option 结构完整它会自动对比新旧数据必要时清除旧图形。但如果你不传入series只传xAxis图表会保留旧的 series 不变这可能导致数据不更新。最稳妥的做法是每次把整个 option 完整传入或者至少在数据变化时连同series一起更新。如果你的项目需要从多个接口拉数据比如一个接口返回趋势数据一个接口返回占比数据建议把这些图表实例分别管理每个图表对应一个更新函数。如果需要所有数据都拿到之后再一起渲染可以用Promise.all把多个 Ajax 请求组合起来处理完再统一setOption避免图表刷新不同步出现闪烁。4.3 图表自适应和 resize 的正确姿势大屏项目离不开窗口缩放自适应最常见的写法是window.addEventListener(resize, function () { chart.resize(); });如果页面有多个图表就分别调用每个实例的 resize。这里有个容易踩的坑如果容器被 CSS 动画或者布局变化改变了尺寸单纯调用resize()可能不够因为它读取的是容器当前宽高而 canvas 内部会重新计算。遇到这种情况可以先手动给容器设置一个具体的像素宽高再调用resize()。另一个更隐蔽的问题如果图表所在的容器初始状态是隐藏的比如标签页切换、折叠面板ECharts 初始化时读到的容器宽高是 0图表会渲染成一个空白的 canvas。等你把容器显示出来再调用 resize图表可能已经处于异常状态。解决办法是在初始化前确保容器可见或者用ResizeObserver监听容器尺寸变化const observer new ResizeObserver(function () { chart.resize(); }); observer.observe(document.getElementById(chart));ResizeObserver是浏览器原生 API能在容器尺寸真正变化时触发回调比单纯监听 window 的 resize 更准确。遇到侧边栏收起、标签页切换这些场景不用担心图表显示异常。另外页面离开时需要清理资源。单页应用里组件销毁时调用chart.dispose()把实例释放掉同时移除对应的 resize 监听避免内存泄漏。重复初始化同一个容器而不提前 dispose控制台会报 There is a chart instance already initialized on the dom 的警告这是很常见的排查线索。5. 几个让 ECharts 悄悄出问题的隐性坑最后这段是真正的排雷经验每一个我都实际碰到过而且排查起来都挺费劲的。它们不像配置错误那么明显不会直接报错但图表表现出来就是不对或者在某些特定环境下突然崩掉。5.1 pxtorem 对 ECharts 没效果问题出在哪热搜词里pxtorem 对echarts没起到效果 vue3这个问题我在 Vue 3 项目里遇到过不止一次。先说结论这不是 ECharts 的 bug也不是 pxtorem 没生效而是两者的工作机制天然冲突。Vue 项目里常用postcss-pxtorem把 CSS 里的 px 自动转成 rem配合 rem 方案做移动端或大屏适配。ECharts 初始化时会读取容器 DOM 的clientWidth和clientHeight这两个属性返回的是 CSS 像素值。如果你的容器宽度在 CSS 里写的是width: 600pxpxtorem 把它转成了width: 37.5rem浏览器在计算时再把 rem 转回像素。理论上最终容器实际宽度是 600pxECharts 读取到的也是 600px看起来应该没毛病。但问题出在边界情况。rem 转换依赖根元素的 font-size如果根字号是响应式的比如根据屏幕宽度动态计算那么容器实际宽度和视觉稿的像素比例就会始终存在一个换算差值。ECharts 内部读取到的是计算后的实际像素宽度canvas 按这个尺寸绘制本来也没问题。真正让你感觉pxtorem 对 ECharts 没效果的场景通常是某个图表的宽度是固定的视觉稿像素比如 1920px 设计稿下图表宽度 480px你用 rem 换算之后在不同设备上计算出来的实际宽度不再是 480px图表元素就会偏大或偏小看起来就像样式没生效。我采用的解决方案有两个方向。一是对 ECharts 的容器单独忽略 rem 转换PostCSS 配置里加排除规则让容器的宽高始终用 px 控制。二是用 JS 动态获取容器实际像素尺寸来初始化图表不依赖 CSS 的固定值const el document.getElementById(chart); const { width, height } el.getBoundingClientRect(); const chart echarts.init(el, null, { width: width, height: height });getBoundingClientRect返回的是元素经过所有样式计算后的实际像素值用这个值作为初始化宽高可以最大程度避免 rem 换算带来的额外影响。如果后续根字号变了再用 resize 重新适配。记住一点ECharts 的 canvas 尺寸是 JS 在初始化时确定的它和 CSS 的关联本质上是读一次容器的实际像素值这种一次性操作搞清楚这一点就不会被没生效的说法带偏。5.2 容器初始化时宽度为 0图表变成空白这个坑的场景通常是这样页面有个 Tab 切换第一个 Tab 是 ECharts 图表页面加载时默认显示第二个 Tab用户切到第一个 Tab 时图表区域是空白的。原因是显而易见的——ECharts 在初始化的时候容器处于display: none状态它读取到的宽高都是 0。很多人会尝试在 Tab 切换完成后手动调用chart.resize()但有时候依然不生效因为容器虽然显示出来了但尺寸变化没有触发 ECharts 的重绘而且如果初始化时宽高是 0canvas 已经被设置成了 0 大小后面恢复尺寸时动画可能出现异常。排查这一类问题不是去反复调试 option而是检查初始化时机。我的做法是确保图表容器在页面上可见之后再调用echarts.init。如果是 Vue 或 React可以把图表的初始化放到 tab 内容渲染完成且容器可见之后执行如果是原生 JS在切换 Tab 到图表所在页签之后用requestAnimationFrame或者setTimeout延迟一下再初始化确保浏览器完成布局和绘制。如果确实无法保证初始化时容器可见也可以先用一段占位样式给容器设置固定的最小宽高比如min-height: 300px让 ECharts 至少有一个可用的高度。总而言之这个问题的排查思路是先确认容器在初始化那一刻的实际宽高再谈配置对不对。5.3 实例复用、销毁和 setOption 的时机ECharts 实例的生命周期管理是很多项目从 demo 走向生产环境时最容易出问题的环节。最常见的错误是在同一个 DOM 节点上反复调用echarts.init没有及时dispose导致页面卡顿甚至内存泄漏。单页应用里进入一个路由页面初始化了一个图表离开路由时如果没有销毁再回来时又初始化新的图表旧实例还占用着 canvas 资源。正确做法是组件卸载时调用chart.dispose()。如果你用的是 Vue 3可以在onBeforeUnmount钩子里处理let chart null; onMounted(() { chart echarts.init(el.value); chart.setOption(option); }); onBeforeUnmount(() { if (chart) { chart.dispose(); chart null; } });还有一类问题出在数据更新太频繁。如果你的图表数据每秒钟都在变比如实时监控大屏反复调用setOption时需要留意是否传了notMerge。默认情况下setOption(option)是 merge 模式它会把新旧配置合并理论上性能更好但如果数据是实时变化且结构经常调整可能出现旧配置残留。这时候可以显式传第二个参数chart.setOption(option, true); // notMerge 为 true直接用新配置替换旧配置但这个参数不能无脑加。notMerge: true会丢弃所有旧配置包括你之前设置过的legend、tooltip等组件如果新传入的 option 没有包含这些组件图表上的交互组件就会消失。我处理实时更新数据时通常把 option 结构定义在一个函数里数据变化后完整地生成一份新 option 传入保持配置的自洽性。最后一个建议当图表使用setOption更新数据时如果你希望更新过程不带动画可以传第三个参数lazyUpdate或者配合opts参数里的silent。实测下来高频数据更新时关闭动画能明显降低 CPU 占用。这些细节看起来无关紧要但在数据大屏这种长时间运行、多图表并存的场景里积累起来就是稳定性差异。关于 ECharts 的社区我多说两句。官方示例库ECharts Gallery里的作品质量参差不齐很多年代久远的示例用的是 3.x 语法直接拷贝到 5.x 项目里可能会报错。遇到问题最好的去处是 GitHub 仓库的 issues 区比如地图数据匹配、labelLine 偏移这些大概率已经有人问过。搜索时记得带上版本号不同版本之间的行为差异很大。我平时排查 ECharts 问题时习惯直接在控制台打印完整的 option逐步缩小排查范围这个笨办法比直接看源码高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑