ECharts 世界地图不显示?GeoJSON 注册与调优指南
简介面向需要在地理数据可视化中集成世界地图的前端开发者这份ECharts世界地图JS资源提供了可直接引用的地图组件与配套脚本。无论是数据大屏、全球业务分析还是地理信息展示都能借助它快速生成可交互的矢量地图。资源包含ECharts核心库、macarons等主题文件以及完整的GeoJSON格式世界地图数据覆盖各国家和地区边界坐标。压缩包共77个文件由39个JavaScript脚本和38个JSON数据文件组成整体约3.42MB。JS脚本负责图表渲染与交互逻辑JSON文件存储地图边界与区域信息结构清晰便于按需引用或二次定制。目前已有1639人学习下载适合前端工程师、数据分析师以及ECharts初学者参考。通过配套配置示例可重点理解地图初始化、数据映射、tooltip提示、visualMap颜色分级等关键用法缩减查阅文档和调试配置的时间是搭建全球分布类图表的高效工具包。1. echarts 世界地图 JS为什么地图没显示先检查数据不是画布开发可视化大屏时世界地图总是一个绕不开的底图需求。echarts 世界地图js 就是把一份带经纬度的 GeoJSON 数据注册进 echarts再用series-map把业务数值映射到各个区域颜色上。很多初学者第一反映是“画布没初始化”但我的血泪经验是绝大多数“地图空白”“区域不显示”问题根因都出在地图数据本身而不是 echarts 的语法。echarts 5 之后不再内置world.js需要自己找 GeoJSON、自己注册、自己处理区域名对齐这一步成了整张地图能不能出来的分水岭。这份资源适合正在做全球业务分布、需要地图下钻、或者被区域命名折腾过的开发者看完能直接落地。2. 注册世界地图如何把 GeoJSON 变成 echarts 认得的组件2.1 为什么 echarts 5 之后必须自己准备 GeoJSON在 echarts 5 之前官方包里还有一份世界地图数据直接require(echarts/map/js/world)就能注册。但从 5.x 开始内置地图数据被移出核心包官方理由也很直白地图数据更新频率高、体积大不该和图形库绑在一起。这就带来一个实际变化任何用到世界地图的项目都得自己找一份世界 GeoJSON然后显式调用echarts.registerMap。我一般会去开源数据社区找一份带国家边界的world.json而不会用那种只有简笔轮廓的静态图片。区别在于静态图片没法做 visualMap 填色也没法绑定区域点击事件。GeoJSON 里面是一堆Feature每个Feature的properties.name就是后续数据和它匹配的关键字段。我的习惯是拿到文件先不看渲染效果先打开 JSON 检查其features数组长度和properties里有没有name字段。如果name字段是中文而后端接口返回的也是中文那后续就少一道翻译的工序如果是英文就要考虑做一张中英文映射表。2.2 registerMap 注册与基础地图渲染先看一段最基础的注册和渲染代码这里我用虚拟区域名演示替换逻辑import * as echarts from echarts; import worldGeoJSON from ./world.json; // 下载好的世界地图 GeoJSON // 第一步全局注册地图名称必须在 setOption 之前完成 echarts.registerMap(world, worldGeoJSON); // 第二步初始化实例 const chartDom document.getElementById(map-container); const myChart echarts.init(chartDom); // 第三步基础配置 myChart.setOption({ series: [ { type: map, map: world, roam: true, selectedMode: single, data: [ { name: 北部区, value: 86 }, { name: 中部区, value: 120 }, { name: 南部区, value: 52 } ] } ] });这段代码里最关键的是registerMap(world, worldGeoJSON)。第一个参数是注册名第二个参数是整个 GeoJSON 对象不能传字符串路径。setOption里map: world必须和注册名完全一致大小写都不能错否则 echarts 会认为地图不存在画布上什么都不显示。data数组里的name要和 GeoJSON 中对应区域的properties.name匹配匹配不上的区域就保持默认底色也不会进入 visualMap 的颜色映射。再强调一个容易忽略的点registerMap只需要执行一次但如果项目里用了多个图表实例每个实例的series-map都能用同一个注册名。我遇到过一个项目因为每次切换页面都重新加载一份地图数据导致注册覆盖时出现样式闪烁。解决办法是先判断echarts.getMap(world)如果已经存在就不再重复注册。2.3 地图数据合法性校验避免把坏数据送进渲染层拿到 GeoJSON 后我强烈建议先跑一遍校验不要直接丢给registerMap。校验只看三点features是否为空、features[0].geometry是否存在、properties.name类型是否为字符串。写一个简单的 Node 校验脚本就能挡住大多数问题const worldGeoJSON require(./world.json); const features worldGeoJSON.features; if (!Array.isArray(features) || features.length 0) { throw new Error(GeoJSON features 为空); } const invalid features.filter(f { return !f.geometry || !f.properties || typeof f.properties.name ! string; }); if (invalid.length 0) { console.warn(存在不合法的 Feature 数量:, invalid.length); } else { console.log(校验通过共, features.length, 个区域); }注意这里校验的是“结构合法性”不是“边界准确性”。结构合法只能保证地图能渲染出来边界数据到底来自哪个机构、是否最新那是另一层问题。涉及具体区域边界时我建议核对数据来源是否权威这是上线前必须确认的合规动作。3. series-map 核心参数调优visualMap、roam 与 label 的显示手感3.1 series-map 和 geo 组件两种世界地图实现方式的取舍绘制世界地图时有两个常见方案直接在series里写type: map或者单独配置geo组件再关联 series。实际开发中两者可以互相配合但它们的分工不一样。方案适合场景注意点纯 series-map页面只有一张地图数据绑定简单visualMap 和 tooltip 配置相对集中geo 组件 series-map需要地图做背景、再叠加散点或航线geo 的 roam 和缩放优先级高于 series我大多数情况下用geo series-map的组合geo管地图底层的缩放、拖拽、视野series管业务数据的颜色映射。这样做的好处是后续如果要叠加飞线图不用复制一套地图配置。如果只是简单展示一张填色图直接一个series-map就够了两种方式我都用过没有谁绝对好看是否要叠加其他系列。3.2 visualMap 的连续与分段数据跨度大时该怎么选visualMap 是地图填色的关键配置。数据从个位数到上万时连续型 visualMap 很容易把大多数区域变成同一个浅色失去区分度。此时应该考虑分段型。先看连续型的典型配置visualMap: { type: continuous, min: 0, max: 1000, calculable: true, inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4] }, text: [高, 低], left: left }min和max直接决定颜色映射区间。如果数据量级突然变大比如最大值变成 10000而max还写死成 1000那超过 1000 的所有区域都会显示最深色。我一般不用写死的最大值而是根据数据动态计算max Math.ceil(Math.max(...values) / 100) * 100这样颜色分布不会被极端值压扁。如果数据跨度特别大或者产品要求看“高、中、低”三档用分段型 visualMap 更直观visualMap: { type: piecewise, pieces: [ { min: 1000, label: 高密集 }, { min: 100, max: 999, label: 中密集 }, { min: 0, max: 99, label: 低密集 } ], inRange: { color: [#d73027, #fee08b, #1a9850] } }用pieces时每段的边界要注意写清楚min和max区间重叠会导致 echarts 取最后匹配的那一段。我习惯把每段写成闭合区间例如{ min: 100, max: 999 }再用注释标注这是含 999 的闭区间。3.3 roam、center 和 zoom地图交互的默认手感roam控制地图能否缩放和拖拽。它有几种取值false禁止所有交互true允许缩放和拖拽scale只允许缩放、不允许拖拽move只允许拖拽、不允许缩放。世界地图默认视野是整个地球产品经常会要求“进来先聚焦到某个业务区域”这时就要手动设置center和zoomseries: [ { type: map, map: world, roam: true, center: [105, 35], zoom: 1.4 } ]center接收经纬度数组[105, 35]是中心点坐标zoom是缩放级别1 表示原大小。注意center作用在地图投影后的平面上它在数据更新后可能会被重置所以每次setOption更新数据时如果没有重新传入center和zoom地图视野会回到默认的全世界视角。这是很多人调了半天觉得“图表自己会跳”的原因。label的显示也需要单独控制。世界地图上如果每个国家都显示名称文字会叠成一团。我的做法是默认关闭 label只在鼠标悬停时显示label: { show: false }, emphasis: { label: { show: true, color: #333, fontSize: 12 } }如果业务上要求常驻显示几个重点区域的名称可以在label.formatter里做过滤只返回需要显示的名称。这个比调整label全局开关要灵活得多。4. 动态更新与地图下钻setOption 与点击区域联动4.1 setOption 分段更新不要每次重建图表很多图表的性能问题都出在“每次数据刷新就重新 init 一个图表实例”。其实 echarts 的setOption支持按需合并只需要把变化的data重新传一次地图的视觉映射会自动更新function updateMapData(newData) { myChart.setOption({ series: [ { type: map, map: world, data: newData } ] }); } // 示例从接口拿到数据后传入 updateMapData([ { name: 北部区, value: 99 }, { name: 中部区, value: 130 } ]);这里的一个关键细节是setOption默认是“合并模式”也就是说传入的 series 配置会覆盖同名 series但其他例如tooltip、visualMap的配置如果不传就不会变化。所以数据更新时我只传series避免把用户正在拖拽的视野也一并重置。如果非要重置视野就在同一个setOption里显式传入geo.center和geo.zoom。4.2 点击区域下钻dispatchAction 与 nameMap 配合世界地图最常见的交互是“点击某个区域放大到该区域并展示明细数据”。监听点击事件myChart.on(click, function (params) { if (params.componentType series params.seriesType map) { const regionName params.name; highlightRegion(regionName); } });拿到区域名后可以用dispatchAction实现视野聚焦和区域高亮。常见的做法是触发geoRoam缩放同时用highlight让当前区域保持高亮状态function highlightRegion(regionName) { myChart.dispatchAction({ type: geoRoam, geoIndex: 0, zoom: 1.8 }); myChart.dispatchAction({ type: highlight, seriesIndex: 0, name: regionName }); }注意geoRoam里的zoom是最终的缩放倍率而不是在当前基础上再放大多少。所以如果想实现“点击两次从 1.8 变成 2.2”需要先通过getOption()读当前zoom再传入新值。highlight的name必须与当前区域名完全一致否则高亮不生效。如果区域名在 GeoJSON 里是英文而界面上要显示中文可以在geo组件里配置nameMapgeo: { map: world, nameMap: { north-area: 北部区, south-area: 南部区 } }nameMap的 key 是 GeoJSON 中的原名value 是展示名。配置之后点击事件回调里的params.name也会变成中文此时再拿去匹配后端明细接口就要注意字段到底是英文还是中文。4.3 返回上级重置视野与清除高亮下钻之后必须提供“返回上级”的操作。重置视野最简单的实现是把zoom设回 1清空center同时清除高亮状态function resetMapView() { myChart.dispatchAction({ type: downplay, seriesIndex: 0 }); myChart.setOption({ geo: { center: null, zoom: 1 } }); }这里有一个实际遇到过的问题把center设为null不会生效应该直接把它从配置里删除。改法是不传center字段并且把zoom显式设为 1这样 echarts 会自动回到默认视角。如果项目里同时存在多个 seriesdownplay时要把seriesIndex和name都传全避免误清除其他系列的高亮。5. 常见问题排查四类地图显示异常的现象与修复地图这块的坑比较集中下面按我实际排查的顺序整理成“现象 → 原因 → 解决”的格式遇到异常可以直接对照。5.1 地图完全空白只有坐标轴或只剩背景色现象图表区域有底色网格或标题正常但没有任何地图轮廓。原因最常见的是registerMap传入的注册名和series.map不一致或者registerMap执行失败后代码没有提示。另一个高频原因是 GeoJSON 本身是普通 JSON 但不是地理数据比如下载成了简化后的边界列表。解决先看控制台有没有报错。然后执行console.log(echarts.getMap(world))如果返回空对象说明注册没成功。再检查导入的文件内容确认features[0].geometry.type是Polygon或MultiPolygon。我排查的顺序是注册名 → 文件内容 → 是否重复注册。5.2 地图渲染了但区域位置明显偏移或形状错乱现象地图能显示但某几个区域跑到海里或者整体扭曲和底图的经纬度对不上。原因世界地图 GeoJSON 存在多种投影坐标系。如果 JSON 里混入了不统一的坐标范围echarts 默认按 GeoJSON 的coordinates直接绘制投影不一致就会出现错位。解决替换 GeoJSON 数据源选与 echarts 坐标系兼容的统一投影数据。不要用本地缓存里“能用但有点歪”的旧文件这种问题后期很难修。另外检查文件里的bbox或crs字段如果出现非 WGS84 的坐标系优先先做一次坐标转换再注册。5.3 区域填色全是默认色visualMap 不生效现象地图轮廓正常数据也传了但所有区域都是同一个灰色或同一个基础色。原因data数组中的name和 GeoJSON 里的properties.name对不上。可能是中英文差异、带不带空格、全角半角符号甚至编码问题。echarts 匹配不到时不会报错只会默默跳过。解决先输出features[0].properties.name看看真实字段值。再对比data里的 name。如果两个都是正确的但数量很大写一个脚本做集合对比输出差集。差集往往是英文全名和缩写之间的差异或数据里带了-1之类的额外字符。5.4 滚轮缩放后视野被重置或者点完一个区域回不到原状现象第一次 zoom 正常但每次数据刷新后地图突然回到全球视野或者点击某个区域放大后找不到方法还原。原因setOption合并模式不会保留没传入的zoom和center图表内部会回到默认值。而还原时把center设为null又不会触发重置导致操作看似无效。解决在每次更新数据时如果产品要求保持视野就把当前zoom和center从getOption()读出来原样合并传回。重置时不要传center只显式传zoom: 1。我习惯把它封装成一个方法所有数据刷新都走这个方法避免遗漏。6. 进阶技巧裁剪世界地图 JSON 并预生成中英文名映射世界地图 GeoJSON 通常有几百 KB 到几 MB对首屏加载很不友好。如果项目只用到十几个国家可以提前裁剪掉多余的features把体积缩到原来的十分之一。这个过程我在本地跑一个 Node 脚本完成输出一份精简后的 JSON 和一个映射表。const worldGeoJSON require(./world.json); // 这里放你业务实际需要的区域英文名替换成你自己 GeoJSON 里的 properties.name const targetAreas [area-a, area-b, area-c]; const filterFeatures worldGeoJSON.features.filter(f targetAreas.includes(f.properties.name) ); const miniWorld { ...worldGeoJSON, features: filterFeatures }; // 同步生成 nameMap供前端渲染中文名 const nameMap {}; filterFeatures.forEach(f { nameMap[f.properties.name] f.properties.name_cn || f.properties.name; }); const fs require(fs); fs.writeFileSync(./world-mini.json, JSON.stringify(miniWorld)); fs.writeFileSync(./name-map.json, JSON.stringify(nameMap));这里的关键是targetAreas必须和 GeoJSON 里实际的properties.name完全一致。我最早做裁剪时直接拿中文名去匹配结果裁剪出来只有零个区域。现在有好几个项目都产出了自己的地区清单我会统一放在一个配置文件里维护避免每次搜索。裁剪后的数据在前端使用没有任何区别仍然是registerMap注册。加载体积变小后首屏渲染速度明显改善。nameMap 文件随后在geo组件里展开使用import nameMap from ./name-map.json; geo: { map: world, nameMap: nameMap }从那以后我每次接到地图需求都会先强制走一遍校验和裁剪脚本先用脚本确认features的结构再检查name是否能对上后端接口最后才把精简的文件交给前端页面。这个动作省掉了大量“看起来是 echarts 配置问题实际是数据不匹配”的排查时间。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

新一代Node中间层开发模式:从胶水层到模块化架构的工程实践

新一代Node中间层开发模式:从胶水层到模块化架构的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:15:02 阅读更多 →
手艺人专属个人项目管理系统:从灵感到作品归档的轻量数字化方案

手艺人专属个人项目管理系统:从灵感到作品归档的轻量数字化方案

artcraft这个名字,字面拆开就是art加craft——艺术和手艺。它不是一个装修风格,也不是某个文创品牌,而是一套我自己从零搭建的、面向手工艺创作者的个人项目管理系统。说白了,就是给做木工、陶艺、皮具、编织、首饰、模型涂装这些…

2026/10/16 3:15:02 阅读更多 →
最长公共子序列(LCS)动态规划详解:从暴力递归到滚动数组优化

最长公共子序列(LCS)动态规划详解:从暴力递归到滚动数组优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:15:02 阅读更多 →

最新新闻

STM32F407单相PWM整流器实现单位功率因数与能量回馈

STM32F407单相PWM整流器实现单位功率因数与能量回馈

简介:本资源是针对2022年全国大学生电子设计竞赛A题「单相交流电子负载」的完整嵌入式实现方案,面向STM32初学者、电赛备赛学生及电力电子方向实践者,重点解决单相系统下高精度锁相与PWM整流控制这一核心难点。工程基于STM32F407VET6主控&…

2026/10/16 4:09:14 阅读更多 →
NAS实战:部署RegexVis让正则表达式可视化

NAS实战:部署RegexVis让正则表达式可视化

正则表达式这玩意儿,真的是让NAS玩家又爱又恨。爱的是它一把梭就能搞定批量重命名、日志筛选、下载文件整理这些脏活;恨的是你写的正则三天后自己都看不懂,更别说改别人留下的“天书正则”了。我前几天在NAS上想整理一批剧集文件,…

2026/10/16 4:09:14 阅读更多 →
Redis为什么快?从内存、IO模型到线程模型的深度拆解

Redis为什么快?从内存、IO模型到线程模型的深度拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:09:14 阅读更多 →
Glide 图片加载缓存机制:弱引用与引用队列的设计原理

Glide 图片加载缓存机制:弱引用与引用队列的设计原理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:09:14 阅读更多 →
车云协同十年演进:从电话式连接到端云智能共同体

车云协同十年演进:从电话式连接到端云智能共同体

车云协同这个词,十年前我拿着方案去讲的时候,说得再热闹,对方核心诉求也就是"能远程开个空调、查个位置就挺好"。那时候车是车,云是云,中间靠一张SIM卡打电话。十年后再看,云端已经成了智能汽车的…

2026/10/16 4:09:14 阅读更多 →
STM32电子密码锁仿真工程:状态机+按键消抖+LCD驱动+Flash模拟EEPROM

STM32电子密码锁仿真工程:状态机+按键消抖+LCD驱动+Flash模拟EEPROM

简介:本资源是一套基于STM32F103C6芯片设计的电子密码锁完整嵌入式开发工程,面向嵌入式初学者、单片机课程设计学生及STM32入门开发者,聚焦密码验证、LCD显示、矩阵键盘输入与蜂鸣器提示等典型功能实现,助力理解HAL库驱动开发与硬…

2026/10/16 4:08:13 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →