省地图加 tooltip 自定义提示框,这俩需求放在一起,基本就是数据大屏、政务看板、区域分析平台里最典型的组合。我最初接类似需求时,也以为 ECharts 自带的地图 tooltip 够用,结果一上真实业务数据就发现,默认展示项远不够,样式也对不上视觉稿,折腾一圈踩了不少坑。这篇直接把我用 ECharts 做省地图和自定义 tooltip 的完整思路、代码、参数细节全部拆开来讲,适合刚接触 ECharts 地图、以及想在 tooltip 上做深度定制的前端同学参考。
1. 先把地图跑起来:省地图实现的完整链路
1.1 省地图数据从哪来:GeoJSON 的正确获取姿势
ECharts 本身不内置省市地图数据,这是新手最容易卡住的地方。你要展示一个省的地图,必须先拿到这个省的地理边界数据(GeoJSON 格式),然后通过 ECharts 的registerMap接口注册,才能把地图渲染出来。
我自己常用的获取渠道有三个:
- 阿里云 DataV GeoAtlas(
datav.aliyun.com/portal/school/atlas/area_selector)——免费,支持中国地图、各省、市、区县四级联动下载,数据更新及时,也是很多项目的主力方案。 - ECharts 官方地图数据仓库(
github.com/apache/echarts的map/json目录)——官方维护,虽然更新频率一般,但胜在稳定。 - 第三方服务生成的定制 GeoJSON——比如需要带某个街道、某个园区边界时,这类数据往往要自己用 GIS 工具或从测绘部门拿。
下载时要注意两点:一是确认你下载的数据是省级边界还是市级边界,别把省地图数据当市地图用;二是检查 GeoJSON 里features数组每一项的properties.name字段,这个名字会对应 ECharts 的name属性,后面数据映射全靠它,如果名称不一致,地图显示正常但数据一律匹配不上。
1.2 注册地图与基础配置代码
拿到 JSON 文件后,在项目里注册地图然后初始化。以 Vue 3 + ECharts 5 为例,核心代码如下:
import * as echarts from 'echarts'; import guangdongJson from '@/assets/map/guangdong.json'; // 1. 注册地图 echarts.registerMap('guangdong', guangdongJson); // 2. 初始化图表实例 const chart = echarts.init(document.getElementById('mapChart')); // 3. 基础配置 const option = { geo: { map: 'guangdong', roam: true, // 允许缩放和拖拽 itemStyle: { areaColor: '#1a2a4a', // 区域填充色 borderColor: '#3a5f9e', // 边界线颜色 borderWidth: 1 }, label: { show: true, color: '#fff' } } }; chart.setOption(option);这里有个容易被忽略的细节:注册地图时第一个参数是你要自定义的地图名称,第二个参数是 GeoJSON 数据。接下来geo.map里用的名称必须和registerMap的第一个参数一致,大小写、空格都要一模一样,否则直接报错。
如果要用series的map类型(这种更常见,因为能配合visualMap做数据映射),写法如下:
const option = { visualMap: { min: 0, max: 1000, inRange: { color: ['#0b1f3a', '#3b8cff', '#6bc5ff'] } }, series: [{ type: 'map', map: 'guangdong', roam: true, data: [ { name: '广州市', value: 986 }, { name: '深圳市', value: 1204 } // ... ] }] };geo和series-map都能显示地图,区别在于:单独的geo组件只负责地图底图展示,不负责数据映射;而series-map可以绑定data,把数值渲染到地图区域上,配合visualMap做颜色渐变更自然。两个同时使用时还能实现“地图底图 + 散点/飞线”叠加效果,这个后面实战部分会讲到。
1.3 地图常用配置项:visualMap、roam、label 设置
把地图配置项过一遍,都是高频使用的,直接照着抄即可。
roam控制缩放拖拽,可选true、false、'scale'、'move'。项目里一般用true,如果只允许缩放不允许拖动就设'scale'。默认地图缩放是有动画的,若不想要,可配合animationDurationUpdate: 0。
visualMap是数据映射的开关,也是最常用配置。需要根据业务数据动态设置min和max,不然颜色映射会失真。例如数据最大值是 1500,但max设成 10000,整个图会一片浅色,看不出层级。
label控制是否显示地区名称。地图比例小的时候显示所有名称会很挤,可以用labelLayout: { hideOverlap: true }自动隐藏重叠标签,ECharts 5 支持这个特性。
emphasis控制鼠标悬浮时的高亮状态,通常需要自定义悬浮区域颜色和标签样式:
emphasis: { itemStyle: { areaColor: '#ffb03a', shadowColor: 'rgba(255, 176, 58, 0.4)', shadowBlur: 20 }, label: { color: '#fff', fontWeight: 'bold' } }地图的select状态(点击选中)和emphasis类似,如果业务需要点击选中某个城市并联动其他图表,就可以定义selectedMode: 'single'或'multiple'。
2. tooltip 自定义提示框:从默认到完全自主可控
2.1 默认 tooltip 长什么样,为什么业务场景总不够用
ECharts 默认的 tooltip 是白底黑字的小浮层,能显示seriesName、name和value。对很多场景确实够用,但凡是做数据可视化大屏,或者视觉上有要求的系统,默认样式几乎都会被毙掉。
默认 tooltip 的几个痛点:
- 样式丑:白底、黑色字体、锋利的直角,和大屏深色背景格格不入。
- 内容单一:只能显示系列名和值,无法自由组合其他字段。
- 无法排版:想放图片、多行数据、单位、环比、占比?默认格式做不到。
- 交互受限:默认跟随鼠标移动,位置、延迟、显示隐藏动画都不好控制。
这些问题的答案都是同一个方向:把 tooltip 的trigger设成支持自定义的项,然后用formatter回调函数自己拼 HTML。
2.2 formatter 回调:拼接 HTML 的完整示例
formatter可以传字符串模板,也可以传函数。字符串模板适合简单拼接,函数适合复杂逻辑。实际项目里,数据越复杂,函数写法就越常用。
先看字符串模板的写法:
tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 人' }{b}是数据项名称,{c}是数值,{a}是系列名。这种写法简单,但字段多了以后可读性差。
再看函数写法:
tooltip: { trigger: 'item', formatter: function(params) { // params 是当前悬浮的数据项对象 return ` <div class="tooltip-title">${params.name}</div> <div class="tooltip-body"> <span>数值</span>: <strong>${params.value}</strong> <br/> <span>占比</span>: <strong>${(params.value / total * 100).toFixed(2)}%</strong> </div> `; } }函数里可以通过params获取的数据很丰富,在省地图场景下,params通常包含:
| 属性 | 含义 |
|---|---|
params.name | 地区名称,比如“广州市” |
params.value | 数值,可能是数组(坐标类时)或单个数值 |
params.seriesName | 系列名称 |
params.data | 传入 series.data 中的原始数据项 |
params.componentType | 组件类型,比如'series'、'markPoint' |
特别注意params.data里可以塞任意自定义字段,这是 tooltip 能做深度定制的前提。比如这样定义数据:
data: [ { name: '广州市', value: 986, population: 1867.66, growth: 3.2, desc: '国家中心城市', tags: ['一线城市', '省会'] } ]然后 formatter 里就能直接读取params.data.population、params.data.growth这些自定义属性,想展示啥就展示啥,自由度非常高。
2.3 样式覆写:让提示框融入你的视觉体系
自定义 HTML 拼出来了,但 ECharts 默认会给 tooltip 外层容器套一层内联样式,比如白色背景、细边框、阴影。这种内联样式优先级很高,直接写 CSS 类很难覆盖,需要从两个方向同时解决。
一是通过tooltip.extraCssText给外层容器追加自定义样式:
tooltip: { trigger: 'item', extraCssText: 'background: rgba(10, 24, 60, 0.9);' + 'border: 1px solid #3a5f9e;' + 'border-radius: 8px;' + 'box-shadow: 0 4px 20px rgba(0,0,0,0.5);' + 'padding: 12px 16px;', formatter: function(params) { // 返回自定义 HTML } }二是直接在formatter返回的 HTML 里把样式写到内联style上,因为内联样式的优先级最高,能覆盖掉外层容器的继承样式。
我个人的习惯是:外层容器的背景、边框、圆角用extraCssText控制,内部元素用内联 style 控制。这样分工清晰,不容易出现“改了背景但内边距不对”这种问题。
2.4 tooltip 换行与富文本:那些细节
tooltip 里的换行是高频需求,很多人一开始直接\n,效果却出不来,因为在 HTML 模式下班\n就是空格。正确做法有两个:
- 在 formatter 返回字符串里直接写
<br/>,这是最常用、最稳定的方式。 - 如果要返回格式为
name: value的多行数据,用<br/>连接每个条目即可。
如果要在 tooltip 里做更丰富排版,比如同行的前后文字对齐、红绿色标记、对齐符号,可以借助 CSS 的display: flex或text-align。下面是一个带对齐效果的 tooltip 示例:
formatter: function(params) { return ` <div style="min-width: 220px;"> <div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: #ffd47e;"> ${params.data.name} </div> <div style="display: flex; justify-content: space-between; margin: 4px 0;"> <span style="color: rgba(255,255,255,0.7);">常住人口</span> <span style="color: #fff; font-weight: bold;">${params.data.population} 万</span> </div> <div style="display: flex; justify-content: space-between; margin: 4px 0;"> <span style="color: rgba(255,255,255,0.7);">GDP总量</span> <span style="color: #4ee0a0; font-weight: bold;">${params.data.value} 亿</span> </div> <div style="display: flex; justify-content: space-between; margin: 4px 0;"> <span style="color: rgba(255,255,255,0.7);">同比增长</span> <span style="color: ${params.data.growth > 0 ? '#ff6b6b' : '#4ee0a0'};"> ${params.data.growth > 0 ? '+' : ''}${params.data.growth}% </span> </div> </div> `; }formatter返回的 HTML 里可以使用任意 CSS,不只是 flex,包括 grid、伪元素,只要能写进内联样式或者通过类名让全局 CSS 覆盖的都能生效。这样 tooltip 就能做到和大屏视觉完全统一。
2.5 tooltip 位置与触发机制:控制浮层出现的方式
tooltip 的位置默认跟着鼠标走,但大屏里鼠标经常要移动到图表外面看数据,此时 tooltip 会消失,体验并不好。可以通过position回调控制:
tooltip: { trigger: 'item', position: function(point, params, dom, rect, size) { // point[0], point[1] 是当前鼠标坐标 // size.viewSize[0] 是图表宽度 // size.contentSize[0] 是 tooltip 内容宽度 // 当鼠标在右侧时,把 tooltip 放在鼠标左侧,防止超出边界 return point[0] + size.contentSize[0] > size.viewSize[0] ? [point[0] - size.contentSize[0] - 20, point[1] - 40] : [point[0] + 20, point[1] - 40]; } }位置返回的[x, y]是 tooltip 左上角相对图表容器的坐标。边界检测逻辑并不复杂:先用size.contentSize拿到提示框的实际宽高,再对比size.viewSize图表可视区大小,超出就往反方向偏移。计算时留出 10~20 像素的间隙更美观。
enterable控制 tooltip 是否允许鼠标进入浮层内部。默认是false,鼠标一旦移出数据项,tooltip 就消失。如果希望用户能从 tooltip 内部复制数据或点击链接,需要设成true:
tooltip: { trigger: 'item', enterable: true, // 允许鼠标进入 tooltip hideDelay: 300 // 鼠标离开后延迟隐藏,避免闪烁 }注意,enterable: true在触发方式为'item'的地图上表现比较稳定,如果要在折线图、柱状图里这样用,hideDelay需要调大一些,不然浮层从“数据项”移到“tooltip”的过程中就消失了。
3. 组合实战:省地图 + 动态数据 + 自定义 tooltip 的完整案例
3.1 需求场景模拟
假设现在做一个广东省经济发展数据大屏,需要展示 21 个地级市的 GDP 数据,视觉上要求:
- 地图背景为深蓝色调,悬浮高亮为橙色。
- 悬浮时 tooltip 显示城市名称、GDP 总量、同比增长率、人口、省内排名。
- 增长率正负值用不同颜色展示。
- 地图支持缩放拖拽,并自动适配容器大小。
- 数据每次拉取后地图联动更新。
这些需求几乎就是政务大屏的标配,拆解后其实就是“地图系列 + visualMap 数据映射 + 自定义 tooltip formatter + 响应式适配”。
3.2 完整代码实现
下面给出可直接运行的完整代码,用的是 Vue 3 语法,核心逻辑不依赖框架,新手上手方便,老手也能直接抠走当模板。
<template> <div ref="mapRef" style="width: 100%; height: 700px;"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue'; import * as echarts from 'echarts'; import guangdongJson from '@/assets/map/guangdong.json'; const mapRef = ref(null); let chart = null; // 模拟后端接口返回的数据 const fetchData = () => { return new Promise(resolve => { setTimeout(() => { resolve([ { name: '广州市', value: 28839, population: 1867.66, growth: 2.1, rank: 1 }, { name: '深圳市', value: 32387, population: 1768.16, growth: 3.3, rank: 1 }, { name: '佛山市', value: 12698, population: 961.26, growth: 1.6, rank: 3 }, { name: '东莞市', value: 11200, population: 1053.68, growth: 0.6, rank: 4 }, { name: '惠州市', value: 4977, population: 605.02, growth: 4.2, rank: 5 }, // ... 其余城市按实际数据补充 ]); }, 300); }); }; // 计算省内排名:这里直接按传入数据里的 rank 字段,真实场景可前端按 value 降序排 const genTooltip = (params) => { const data = params.data || {}; const color = data.growth >= 0 ? '#ff6b6b' : '#4ee0a0'; const sign = data.growth >= 0 ? '+' : ''; return ` <div style="min-width: 240px; padding: 4px 2px;"> <div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: #ffd47e; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 6px;"> ${data.name || params.name} </div> <div style="display: flex; justify-content: space-between; margin: 6px 0;"> <span style="color: rgba(255,255,255,0.65);">GDP总量</span> <span style="color: #fff; font-weight: bold;">${data.value ?? '-'} 亿元</span> </div> <div style="display: flex; justify-content: space-between; margin: 6px 0;"> <span style="color: rgba(255,255,255,0.65);">同比增长</span> <span style="color: ${color}; font-weight: bold;">${sign}${data.growth ?? '-'}%</span> </div> <div style="display: flex; justify-content: space-between; margin: 6px 0;"> <span style="color: rgba(255,255,255,0.65);">常住人口</span> <span style="color: #fff;">${data.population ?? '-'} 万</span> </div> <div style="display: flex; justify-content: space-between; margin: 6px 0;"> <span style="color: rgba(255,255,255,0.65);">省内排名</span> <span style="color: #4ee0a0; font-weight: bold;">第 ${data.rank ?? '-'} 位</span> </div> </div> `; }; const renderChart = async () => { if (!mapRef.value) return; // 注册地图:名称建议用连字符或下划线,避免命名冲突 echarts.registerMap('guangdong', guangdongJson); chart = echarts.init(mapRef.value); const data = await fetchData(); const option = { backgroundColor: 'transparent', tooltip: { trigger: 'item', enterable: true, hideDelay: 100, backgroundColor: 'rgba(10, 24, 60, 0.92)', borderColor: 'rgba(90, 140, 220, 0.6)', borderWidth: 1, borderRadius: 8, padding: [12, 16], textStyle: { color: '#fff', fontSize: 13 }, extraCssText: 'box-shadow: 0 6px 24px rgba(0,0,0,0.5);', formatter: genTooltip }, visualMap: { min: 0, max: 35000, splitNumber: 5, left: 20, bottom: 20, text: ['高', '低'], textStyle: { color: '#fff' }, inRange: { color: ['#0b1f3a', '#1a4a8a', '#2f7dcf', '#4ea5f5'] }, show: true }, series: [{ name: 'GDP数据', type: 'map', map: 'guangdong', roam: true, selectedMode: false, label: { show: true, fontSize: 10, color: '#d0dff5' }, itemStyle: { areaColor: '#132a4a', borderColor: '#3a6a9e', borderWidth: 1 }, emphasis: { itemStyle: { areaColor: '#ffb03a', shadowColor: 'rgba(255, 176, 58, 0.5)', shadowBlur: 30 }, label: { color: '#1a1a2a', fontWeight: 'bold', fontSize: 14 } }, data: data }] }; chart.setOption(option); }; // 自适应窗口变化 const handleResize = () => { chart && chart.resize(); }; onMounted(() => { renderChart(); window.addEventListener('resize', handleResize); }); onBeforeUnmount(() => { window.removeEventListener('resize', handleResize); if (chart) { chart.dispose(); chart = null; } }); </script>这段代码拿到真实项目里,改改地图 GeoJSON、改改数据字段名就能直接用。核心注意点是data中每一项的name必须和 GeoJSON 中的地理名称一致,比如“广州市”不能写成“广州”。
3.3 核心参数解释:为什么这么配
tooltip.trigger: 'item':地图是项级触发的,每个地图区域算一个数据项,所以必须用item,不能用axis。axis主要用于直角坐标系图表。
visualMap.splitNumber: 5:颜色分段数量。数据范围跨度大时,5 段比较直观;如果数据差异特别大,可以适当调小,或者干脆不设splitNumber,让 visualMap 自行连续渐变。
series-map的name字段除了作为系列名进入 tooltip 之外,还用于多个 series 的分组标识。如果你在地图上面叠加了散点图、飞线图,name要区分开。
itemStyle.areaColor是地图默认底色。实际项目中建议让底色脱离高亮色,这样visualMap的映射才明显。很多新手把底色设成和 visualMap 最高值颜色相同,结果地图一片亮,完全看不出层次,一定要避免。
emphasis.shadowBlur是悬浮高亮时的光晕模糊半径,用来营造发光效果。数值越大,光晕越明显。透明背景的大屏上,光晕颜色建议带 alpha(如rgba(255, 176, 58, 0.5)),不然会有生硬的实心边缘。
4. 常见问题与排查技巧实录
4.1 地图空白 / 不显示的排查思路
地图加载出来是空白,多半是数据没注册成功。排查顺序如下:
第一,GeoJSON 文件是否真正加载了。可以直接在浏览器 Network 面板搜地图 JSON 文件名,确认请求是否返回 200。
第二,registerMap是否在setOption之前执行。ECharts 的setOption执行时如果发现地图未注册,会直接不渲染,而且默认不报错,很难注意到。
第三,地图名称是否一致。registerMap('guangdong', json)后,series.map必须是'guangdong'。我见过有同事地图 name 用了中文,注册时传了英文,结果折腾一小时。
第四,容器是否有高度。ECharts 初始化时必须保证外层容器已有高度,如果容器height: 0或不设置,图表渲染出来就是白板。建议在mounted后、DOM 布局稳定后再初始化,或者用nextTick包一层。
第五,GeoJSON 是否合法。把下载的 JSON 放到jsonlint.com或浏览器控制台里解析一下,确认没有少括号、多逗号。
4.2 tooltip 样式不生效 / 换行无效
tooltip 样式不生效最典型的原因就是内联样式优先级问题。ECharts 在渲染 tooltip 外层容器时,会写入比较多的内联样式,比如background-color、border、box-shadow。此时你在 CSS 文件里写的.tooltip-container { background: red }会大概率被内联样式盖住。
解决办法有两个方向:
- 所有关键样式都写在
extraCssText或 formatter 返回的 HTML 里。 - 给返回 HTML 最外层元素加上超高优先级 CSS 选择器,并用
!important:
.tooltip-box { background: #0a183c !important; border: 1px solid #3a5f9e !important; }但要注意,formatter 返回的字符串渲染位置在 ECharts 的固定容器内,当你给 tooltip 设置className并希望用外部 CSS 控制时,必须看一下 ECharts 的 DOM 结构,确认类名作用的实际元素,不然经常出现选择器没命中。
换行无效的问题,十有八九是用了\n,要改成<br/>。
4.3 性能优化与内存泄漏问题
地图数据量大、页面频繁切换时,性能问题会被放大。最常见的是内存泄漏:每次进入页面都新建echarts.init,但离开页面时只移除了 DOM,没有调用dispose,最后图表实例越积越多,页面越来越卡。
正确做法:
- 组件销毁前调用
chart.dispose()。 - 每次重新渲染数据时,不要反复
init,而是在同一个实例上setOption(option, true)。第二个参数true会清空旧组件重新渲染,避免多次 setOption 叠加导致的内存上升。 - 数据更新时同样复用实例,只改
series.data和visualMap.max,而不是整包配置全部 set 一次。
另外,地图缩放拖拽时会连续触发渲染,如果 GeoJSON 数据特别精细(比如带街道边界的省地图,几十 MB),会导致卡顿。解决办法是开发环境下用完整数据,生产按需简化 GeoJSON 的顶点密度,或者用roam: 'move'禁掉缩放,减小渲染压力。
4.4 resize 自适应与容器大小动态变化的坑
窗口变化后地图不更新,通常是因为没有调用chart.resize()。这个好解决,监听resize事件即可。但要注意resize事件必须绑定在 window 上,不能绑定在图表容器上,因为容器本身不触发 resize。
比较隐蔽的问题出现在侧边栏折叠、Tab 切换、弹窗打开这类场景。容器从隐藏变为显示时,宽度已经变化,但resize事件不一定触发。此时需要在容器显示后手动调用chart.resize(),或者在nextTick里调用。
还有,如果chart.resize()后地图地理信息变形,说明容器初始宽度不对。解决办法是在初始化前确认容器宽度 > 0,或者强制图表容器定高定宽。
4.5 数据更新后 tooltip 不刷新的问题
这个坑也值得单独说。如果你的 formatter 里依赖外部变量(比如total、颜色主题),但数据更新后 tooltip 显示的还是旧值,原因多半是 formatter 引用的外部变量没有同步更新,或者 tooltip 被缓存了。
排查方法:在setOption更新数据之前,先chart.clear(),然后重新setOption。这样能强制 tooltip 的 DOM 重新渲染。如果你的 formatter 函数体里读取的闭包变量已经变化了,但页面仍显示旧值,请检查一下初始化 chart 的函数是不是被重复调用了两次,导致绑定的是旧实例。
5. 从省地图到多地图联动:扩展玩法与更优雅的写法
5.1 多级下钻与返回
做省地图的下一步常规演进就是“全国地图 -> 点击省份下钻到省地图 -> 再下钻到市地图”。这种场景下要维护一个地图栈:
const mapStack = []; const drillDown = (mapName, geoJson, data) => { mapStack.push(currentMapName); echarts.registerMap(mapName, geoJson); chart.setOption({ series: [{ map: mapName, data: data }] }); }; const drillBack = () => { const prevMapName = mapStack.pop(); chart.setOption({ series: [{ map: prevMapName, data: prevData }] }); };点击事件用chart.on('click', params => { ... })捕获,然后根据params.name加载下一级的 GeoJSON。这里很容易忽略的是:地图切换时如果series.map指向了未注册的地图名,同样会白屏,所以下钻前一定要确保registerMap完成。
5.2 tooltip 里的联动与跳转
formatter 返回的 HTML 里可以写<a>标签或绑定事件,从而实现“点击 tooltip 查看详情页”的效果。但注意,ECharts 的 tooltip 层在enterable: true时才允许鼠标交互。再配合chart.getZr()绑定点击事件,还能做更精细的浮层点击冒泡处理。不过这个属于进阶技巧,新手还是优先保证enterable开启后链接能正常跳转就行。
5.3 结合 ref 封装一个可复用地图组件
最后建议把地图封装成组件,入参为mapName、geoJson、data、tooltipFormatter四个配置,这样一套逻辑可以复用在多个页面。我在实际项目里就是这么封装的,省地图、市地图、区县地图都走同一个组件,只需要切换数据源。
封装时的核心注意点是:registerMap如果对同一个mapName重复注册,ECharts 会直接覆盖,这在多实例下容易引发“页面A传了广东数据,页面B切走后,页面A地图显示成广东”的串数据问题。解决思路是每次注册时用唯一 ID,比如mapName + '_' + Date.now(),或者组件销毁时将地图名称从注册表中移除(ECharts 5.3+ 支持echarts.getMap()查询和替换)。
这块内容实际做下来,最大的体会是:ECharts 地图和 tooltip 的坑大多不是原理难,而是文档之外的细节多。比如名称匹配、内联样式覆盖、resize 时机、实例复用,每一条单独看都不难,但串起来就是几十个坑。遇到问题时别急着搜代码,先把数据、注册、实例生命周期这三个环节过一遍,多半能定位到问题。我自己踩过最深的坑其实是视觉稿极其精美但 tooltip 样式怎么也刷不上,最后发现是extraCssText的padding和 formatter 内部min-width冲突,改掉其中一个,效果立刻正常了。这些小经验,希望新手朋友们少走点弯路。