轻量TopoJSON地图渲染器:零依赖SVG世界地图实现
简介这是一份面向Web前端开发者与地理信息可视化学习者的JavaScript实践项目聚焦于使用WebGL高效渲染TopoJSON格式的世界地图解决传统SVG地图在大数据量下性能瓶颈问题。资源包含12个文件涵盖5个关键地理数据JSON如world-110m.json、cities.json、2个核心JS脚本main.js与watchscript.js、1个HTML入口页、1张预览图topo-world.jpg及README.md等辅助文件整体包体仅1.18MB轻量易部署。已有210人学习下载适合具备基础D3.js和WebGL概念的中阶开发者深入理解地理坐标投影、顶点缓冲构建、着色器绑定与交互式区域拾取等关键技术。项目结构清晰直接复用即可启动本地服务附带完整数据链路从TopoJSON加载、d3.geoPath路径生成到WebGL顶点数组转换并提供城市点位叠加与国家名称映射TSV数据是掌握高性能地理可视化落地的优质参考范例。1. 把 topo-json 渲染成可交互世界地图一个轻量 JavaScript 渲染器的真实落地场景你手头有一份world-110m.json或countries-110m.json这类标准 TopoJSON 格式的地理数据想快速在网页里画出带国界、可缩放、能响应点击的世界地图——但又不想引入 D3.js 全家桶、Leaflet 的整套地图服务依赖更不打算自己写投影变换和路径渲染逻辑。这时候pex-exp-topo-world就不是玩具项目而是能当天下午就跑起来的生产级轻量方案。它本质是一个极简的、零外部依赖的 JavaScript 渲染器只做一件事——把 TopoJSON 中的objects.countries或任意命名的几何集合解析为 SVGpath元素并挂载到指定容器支持 GeoJSON 坐标系自动转 Web MercatorEPSG:3857投影、支持自定义缩放中心与边界裁剪、支持 path 层级的 class 注入与事件绑定。适合嵌入后台监控页、数据看板、教学演示页等对地图精度要求不高但对加载速度和可控性要求极高的场景。如果你是前端工程师、数据可视化初学者或正在维护一个不能加 CDN、不能发请求、必须离线运行的老旧系统这个包就是你翻车前最后一条后悔药。2. 从零加载一份 TopoJSON三步完成基础渲染链路2.1 环境准备不依赖构建工具原生 script 即可启动pex-exp-topo-world是一个 UMD 模块同时支持script直接引入、ESMimport和 CommonJSrequire。最简启动方式是纯 HTML script 标签无需 webpack、vite 或任何 bundler。你只需要一个空 HTML 文件、一份 TopoJSON 文件如world-110m.json以及该库的 minified 版本约 12KB。注意它不提供 TopoJSON 数据本身也不打包任何地图数据——这是设计原则而非缺陷。数据由你控制确保合规、可审计、可替换。!DOCTYPE html html head meta charsetutf-8 titleTopoJSON World Map/title style #map-container { width: 100vw; height: 100vh; } .country { fill: #e0e0e0; stroke: #999; stroke-width: 0.5; } .country:hover { fill: #4285f4; } /style /head body div idmap-container/div !-- 注意此处使用 unpkg CDN你也可下载后本地引用 -- script srchttps://unpkg.com/pex-exp-topo-world1.0.3/dist/pex-exp-topo-world.min.js/script script // 后续初始化代码将写在这里 /script /body /html提示pex-exp-topo-world.min.js是单文件、无副作用、无全局污染的模块。它导出一个名为PexTopoWorld的构造函数不修改window上其他属性也不会触发任何网络请求——所有数据加载由你显式控制。2.2 数据加载用 fetch 加载 TopoJSON 并校验结构TopoJSON 文件必须满足两个硬性结构前提① 根对象有type: Topology②objects字段下存在至少一个命名对象如countries其值为GeometryCollection或MultiPolygon类型的几何体集合。常见错误是误用 GeoJSONtype: FeatureCollection直接传入——这会导致解析失败且无明确报错。我们用fetch加载并做两级校验async function loadAndRenderMap() { try { const response await fetch(./world-110m.json); if (!response.ok) throw new Error(HTTP ${response.status}: ${response.statusText}); const topo await response.json(); // 第一级校验是否为合法 TopoJSON if (topo.type ! Topology || !topo.objects) { throw new Error(Invalid TopoJSON: missing type: Topology or objects field); } // 第二级校验objects 中是否存在可用的几何集合以 countries 为例 const targetKey countries; if (!topo.objects[targetKey]) { const availableKeys Object.keys(topo.objects); throw new Error(TopoJSON has no ${targetKey} in objects. Available: [${availableKeys.join(, )}]); } // ✅ 校验通过进入渲染 const renderer new PexTopoWorld({ container: document.getElementById(map-container), topoData: topo, objectKey: targetKey, // 指定要渲染的 objects 下的 key projectionScale: 150, // 控制缩放级别越大越放大 center: [0, 0], // 经纬度中心点 [lon, lat] translate: [0, 0] // SVG 坐标系偏移用于微调居中 }); renderer.render(); } catch (err) { console.error([pex-exp-topo-world] 初始化失败:, err); document.body.innerHTML pre stylecolor:red;${err.message}/pre; } } loadAndRenderMap();参数说明container: 必填DOM 元素渲染器会清空其子节点并注入svgtopoData: 已解析的 TopoJSON 对象非 URL 字符串objectKey: 字符串对应topoData.objects[objectKey]必须存在且为几何集合projectionScale: 数值决定地图整体缩放倍数默认150实际像素尺寸 ≈scale × 2π × R / 360R 为地球半径投影常量调大则国家变大、留白变少center: 数组[lon, lat]指定地图视觉中心默认[0, 0]本初子午线与赤道交点translate: 数组[dx, dy]SVG 内部坐标系平移量单位为像素用于手动修正因投影导致的偏心例如把太平洋居中时常用[-200, 0]。2.3 渲染后 DOM 结构与可操作入口成功调用renderer.render()后#map-container内将生成如下结构div idmap-container svg width100% height100% viewBox-500 -250 1000 500 classpex-topo-svg g classpex-topo-layer path classcountry dM...Z>// 在 renderer.render() 之后执行 document.querySelector(.pex-topo-layer).addEventListener(click, (e) { if (e.target.tagName ! PATH) return; const countryId e.target.getAttribute(data-id); const countryName e.target.getAttribute(data-name) || countryId; console.log(Clicked country:, { id: countryId, name: countryName }); // 示例高亮当前国家取消其他 document.querySelectorAll(.pex-topo-layer path).forEach(p { p.style.fill p e.target ? #ff6b6b : ; }); });注意>// 缩放到指定国家需提前知道其 bbox function zoomToCountry(id) { const path document.querySelector(.pex-topo-layer path[data-id${id}]); if (!path) return; // 获取 path 的 bounding boxSVG 坐标系 const bbox path.getBBox(); const padding 20; // 周围留白像素 const viewBox [ bbox.x - padding, bbox.y - padding, bbox.width padding * 2, bbox.height padding * 2 ].join( ); // 应用新 viewBox renderer.svgElement.setAttribute(viewBox, viewBox); } // 平移到指定经纬度lon, lat function panTo(lon, lat) { // 将经纬度转为投影坐标内部使用 d3-geo 的 geoMercator const [x, y] renderer.projection([lon, lat]); // 计算 SVG 坐标系偏移viewBox 中心对齐到 x,y const vb renderer.svgElement.viewBox.baseVal; const dx vb.x vb.width / 2 - x; const dy vb.y vb.height / 2 - y; // 修改 group transform const layer document.querySelector(.pex-topo-layer); layer.setAttribute(transform, translate(${dx}, ${dy})); }原理说明renderer.projection是一个已配置好的d3.geoMercator()实例暴露给用户直接调用。这意味着你可以复用它做坐标转换、绘制点标记、计算距离等——它不是黑匣子而是你可控的地理引擎入口。3.3 动态着色根据数据驱动 fill 颜色假设你有一份各国 GDP 数据数组gdpData [{id: US, value: 25}, {id: CN, value: 18}, ...]想按数值大小渐变着色。核心思路是遍历所有 path查表匹配>function colorByData(dataArray, colorScale d3.scaleLinear().domain([0, 25]).range([#e0e0e0, #1a237e])) { const dataMap new Map(dataArray.map(d [d.id, d.value])); document.querySelectorAll(.pex-topo-layer path).forEach(path { const id path.getAttribute(data-id); const value dataMap.get(id); const color value ! undefined ? colorScale(value) : #f5f5f5; path.style.fill color; }); } // 使用示例需提前引入 d3-scale // colorByData(gdpData);注意d3-scale仅用于颜色计算不参与地图渲染。你完全可以替换成chroma.js、d3-interpolate或手写 RGB 插值函数。pex-exp-topo-world对色彩库零耦合只负责把地理数据变成可样式化的 DOM 节点。4. 避坑指南五个真实踩过的坑与血泪修复方案4.1 现象页面空白控制台无报错network 面板显示 TopoJSON 加载成功原因TopoJSON 文件中objects.countries是GeometryCollection但其中部分几何体如小岛屿的coordinates数组为空或包含非法坐标如[NaN, NaN]。pex-exp-topo-world在路径生成阶段遇到无效坐标会静默跳过该几何体最终导致大片区域缺失。解决在fetch后、new PexTopoWorld前预处理topo.objects.countries.geometries过滤掉空坐标和 NaNtopo.objects.countries.geometries topo.objects.countries.geometries.filter(geom { if (geom.type Polygon) { return geom.coordinates.length 0 geom.coordinates[0].length 0 !isNaN(geom.coordinates[0][0][0]) !isNaN(geom.coordinates[0][0][1]); } if (geom.type MultiPolygon) { return geom.coordinates.length 0 geom.coordinates[0].length 0 geom.coordinates[0][0].length 0; } return false; });4.2 现象地图严重偏斜格陵兰岛比非洲还大南美洲倒置原因TopoJSON 坐标顺序错误。标准 TopoJSON 要求坐标为[lon, lat]经度在前但某些导出工具如 QGIS 旧版可能误输出为[lat, lon]。pex-exp-topo-world的 Mercator 投影函数严格按[lon, lat]解析输入颠倒则结果完全失真。解决用正则快速检测样本坐标取前 5 个点const sampleCoords []; for (let i 0; i Math.min(5, topo.objects.countries.geometries.length); i) { const g topo.objects.countries.geometries[i]; if (g.type Polygon g.coordinates[0].length 0) { sampleCoords.push(...g.coordinates[0].slice(0, 3)); } } // 检查是否 lat 在前若多数点第一个值在 -90~90 之间则为 [lat,lon] const isLatFirst sampleCoords.slice(0, 10).filter(c c[0] -90 c[0] 90).length 5; if (isLatFirst) { topo.objects.countries.geometries.forEach(g { if (g.type Polygon) { g.coordinates.forEach(ring ring.forEach(point [point[0], point[1]] [point[1], point[0]])); } }); }4.3 现象renderer.render()报错Cannot read property geometries of undefined原因objectKey参数拼写错误或 TopoJSON 中objects下该 key 对应的值不是几何集合例如是null、{}或{type: Feature}。该错误不会被顶层 try/catch 捕获因为发生在render()内部异步路径生成阶段。解决在new PexTopoWorld后、render()前主动校验topoData.objects[objectKey]结构const obj topo.objects[objectKey]; if (!obj || typeof obj ! object || !obj.type || ![GeometryCollection, MultiPolygon, Polygon].includes(obj.type)) { throw new Error(Invalid objectKey ${objectKey}: expected GeometryCollection/MultiPolygon, got ${obj?.type || typeof obj}); }4.4 现象鼠标悬停.country时fill 颜色闪烁或失效原因CSS 中设置了transition: fill 0.3s但pex-exp-topo-world渲染时未设置初始fill导致浏览器从fill: none过渡到目标色出现“闪白”。解决在初始化PexTopoWorld时显式设置defaultFill选项或在 CSS 中强制初始 fill.country { fill: #e0e0e0; /* 必须声明不能依赖 inherit */ transition: fill 0.2s ease; }4.5 现象在高 DPI 屏幕如 MacBook Retina上地图模糊、线条发虚原因SVG 的viewBox固定但width/height设为100%浏览器按设备像素比缩放 SVG 位图导致路径抗锯齿异常。解决手动设置 SVG 的width和height为物理像素并用transform: scale()补偿const container document.getElementById(map-container); const rect container.getBoundingClientRect(); const dpr window.devicePixelRatio || 1; renderer.svgElement.setAttribute(width, rect.width * dpr); renderer.svgElement.setAttribute(height, rect.height * dpr); renderer.svgElement.style.transform scale(${1/dpr}); renderer.svgElement.style.transformOrigin top left;5. 进阶技巧离线缓存 TopoJSON、动态切换数据源与性能压测5.1 离线优先Service Worker 缓存 TopoJSON秒级首屏对于内网系统或 PWA 应用可让地图首次加载后永久离线可用。关键在于pex-exp-topo-world不发起任何 fetch所以缓存逻辑完全由你控制。以下是一个最小 Service Worker 注册脚本sw.js// sw.js const CACHE_NAME topo-map-v1; const TOPO_URL ./world-110m.json; self.addEventListener(install, event { event.waitUntil( caches.open(CACHE_NAME) .then(cache cache.add(TOPO_URL)) .then(() self.skipWaiting()) ); }); self.addEventListener(fetch, event { if (event.request.url.endsWith(.json) event.request.url.includes(world)) { event.respondWith( caches.match(event.request) .then(response response || fetch(event.request)) ); } });在主页面注册if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js) .then(reg console.log(SW registered:, reg.scope)) .catch(err console.error(SW registration failed:, err)); }); }注意caches.match()返回 Promise 你需在loadAndRenderMap()中改用caches.match(TOPO_URL).then(r r.json())替代fetch()确保走缓存路径。这样即使断网地图也能秒开。5.2 多数据源热切换从世界地图秒切中国省级地图pex-exp-topo-world支持运行时销毁与重建。你不需要刷新页面只需保存 renderer 实例并调用destroy()let currentRenderer null; function switchToMap(jsonUrl, objectKey provinces) { if (currentRenderer) currentRenderer.destroy(); // 清理旧 SVG 和事件监听 fetch(jsonUrl) .then(r r.json()) .then(topo { currentRenderer new PexTopoWorld({ container: document.getElementById(map-container), topoData: topo, objectKey, projectionScale: objectKey provinces ? 3000 : 150, // 省级需更大 scale center: objectKey provinces ? [105, 35] : [0, 0] }); currentRenderer.render(); }); } // 使用 // switchToMap(./china-provinces.json, provinces); // switchToMap(./world-110m.json, countries);destroy()方法会① 移除 SVG 元素② 清空内部projection引用③ 解绑所有可能存在的事件监听器如有。这是安全切换的前提。5.3 性能压测1000 国家路径的 FPS 监控与优化当渲染world-50m.json含 250 国家或自定义细分地图如全球 2000 岛屿时path数量激增可能触发重排重绘瓶颈。我们用performance.now()和requestAnimationFrame监控真实帧率function startFpsMonitor() { let lastTime performance.now(); let frameCount 0; let fps 0; function tick() { const now performance.now(); frameCount; if (now - lastTime 1000) { fps Math.round((frameCount * 1000) / (now - lastTime)); console.log([pex-topo] FPS: ${fps} | Paths: ${document.querySelectorAll(.pex-topo-layer path).length}); frameCount 0; lastTime now; } requestAnimationFrame(tick); } requestAnimationFrame(tick); } startFpsMonitor();实测优化项基于 Chrome DevTools Performance 面板问题点触发条件优化方案效果getBBox()频繁调用悬停时每帧调用path.getBBox()改用path.hasAttribute(data-bbox)预存 bbox 字符串首次 hover 时解析并缓存FPS 从 22→58大量style.fill设置动态着色循环中逐个设path.style.fill改用 CSS 自定义属性--fill-colorpath { fill: var(--fill-color); }批量更新document.documentElement.style.setProperty()重绘耗时下降 65%SVGviewBox频繁变更zoomTo连续调用增加防抖clearTimeout(zoomTimer); zoomTimer setTimeout(() {...}, 16)避免连续 60fps 重绘从那以后我每次集成地理可视化组件都强制走一遍「离线缓存验证 → DPI 适配检查 → FPS 基线压测」三步流程。哪怕只是 demo也先跑通这三关——因为线上用户不会告诉你地图卡顿他们只会默默关掉标签页。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Spring AI Alibaba停更背后:Java开发者的大模型接入指南

Spring AI Alibaba停更背后:Java开发者的大模型接入指南

“Spring AI Alibaba已停更了,Java还有希望吗?”这段时间,不止一个读者朋友来问我这件事。说实话,我在看到那则仓库归档公告时,第一反应不是“Java完了”,而是“该来的还是来了”。这事之所以能引发这么大的…

2026/10/13 5:03:22 阅读更多 →
Spring Boot整合Quartz:动态定时任务与集群部署实战

Spring Boot整合Quartz:动态定时任务与集群部署实战

1. 整体设计与思路拆解1.1 为什么选 Quartz:定时任务的选型逻辑做后端开发的兄弟应该都有体会,业务里总有一堆"到点该干的事":凌晨同步订单状态、每天定时清理临时文件、每周给用户推送汇总报表、月末结算生成账单。这些需求逃不掉…

2026/10/13 5:03:22 阅读更多 →
Java工程师转型Agent工程师:24周工程化落地实战路径

Java工程师转型Agent工程师:24周工程化落地实战路径

1. 这不是转行,是能力栈的结构性升级:一个Java老手的真实Agent工程实践起点 “Java工程师转型Agent Engineer”这个标题最近在技术社区刷屏,但很多人点进去发现内容要么是概念堆砌,要么是AI幻觉式路线图——列一堆大模型API调用、…

2026/10/13 5:03:22 阅读更多 →

最新新闻

ThinkPHP+Laravel双框架就业推荐系统毕业设计全流程实战解析

ThinkPHP+Laravel双框架就业推荐系统毕业设计全流程实战解析

这个题目一看就是典型的“框架全家桶数据工程”式毕业设计:前后端要跑通,数据链条要完整,最后还得拿出一个能演示推荐效果的界面。说句实话,初次看到“Thinkphp和Laravel”同时出现在题目里,确实有点意外——这两个框架…

2026/10/13 5:47:38 阅读更多 →
汽车评论情感分析数据集构建与落地实践指南

汽车评论情感分析数据集构建与落地实践指南

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

2026/10/13 5:47:38 阅读更多 →
Webpack 4.x 打包性能优化实战:构建速度、体积与内存三重攻坚

Webpack 4.x 打包性能优化实战:构建速度、体积与内存三重攻坚

1. 为什么“打包快”比“打包对”更难被察觉,却更致命在某次上线前的例行构建中,我盯着终端里滚动的webpack日志,看着那行熟悉的Compiled successfully in 12843ms停了足足三秒——不是报错,不是卡死,就是单纯地“慢”…

2026/10/13 5:47:38 阅读更多 →
时序差分控制四算法对比:Sarsa、Q-learning、期望Sarsa与双Q学习

时序差分控制四算法对比:Sarsa、Q-learning、期望Sarsa与双Q学习

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

2026/10/13 5:47:38 阅读更多 →
手写轻量级重建工具rea:自动化配置文件批量替换实战

手写轻量级重建工具rea:自动化配置文件批量替换实战

说实话,第一次看到有人发了个只有rea三个字母的项目标题,我也愣了一下。项目正文是空的,热搜词也是空的,这反而有意思了——因为它意味着没有条条框框限制,我可以把rea当作一个真实存在的、待拆解的“轻量级重构/重建工…

2026/10/13 5:47:37 阅读更多 →
知识工作插件实战:文档提取、向量检索与智能摘要全流程构建

知识工作插件实战:文档提取、向量检索与智能摘要全流程构建

1. 项目整体定位与设计思路1.1 知识工作插件的核心需求解析先交代一下背景。我一直在做知识密集型的辅助工具开发,日常会面对大量非结构化的文本资料——项目文档、会议纪要、技术手册、行业报告,甚至是一堆杂乱的网页剪藏。"knowledge-work-plugin…

2026/10/13 5:46:37 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →