WorkBuddy + Leaflet 实战:从零搭建「湘江战役」历史 GIS 动态态势图(附踩坑与取景算法)

一句话总结:把一句含糊的需求,做成一张能在手机上自己巡展、能直接发链接的战役态势图。

前言

历史事件的可视化,难点往往不在"画地图",而在三件事:地理基准对不对、符号像不像军事标图、时间维度怎么表达才不糊。这篇记录我用 WorkBuddy 从一句需求开始,做出一个包含 10 个时间节点、支持自动巡展与移动端浏览的交互式态势图的全过程,附可复用的取景算法、要素视觉规则表,以及我踩过的 6 个坑。

成品是纯静态网页:Leaflet 1.9.4 + 自研标绘内核,无后端、可离线,首屏不加载音频,手机横竖屏都能用。

【图 1|系统运行图】 桌面端 1440×900 首屏:左侧时间轴(10 个节点)、右侧地图显示第 1 幕、底部播放条。

一、项目背景

1934 年 11 月底,中央红军在广西全州、兴安、灌阳之间强渡湘江,突破第四道封锁线;此役过后,红军由八万六千人减至三万余人。我想把这段过程做成一张可交互的态势图,让读者按时间推进,看战场是怎样一步步变化的。

我最初只给了 WorkBuddy 一句需求:"基于 Leaflet 和军事标绘 plot,介绍湘江战役,带时间轴。"它没有直接开工,先指出两点:主战场在广西,不在长沙;"军事标绘"查到的多是北约军标体系,没有可离线引用的现成包。歧义摊开之后,项目范围才真正定下来。

这里有个可复用的判断: 凡是"历史地名 + 地理可视化"的需求,第一步一定是核对坐标系上的基准点,而不是先写渲染代码。基准错了,后面所有标注、视野、箭头全部要返工。

二、技术选型

层次 方案 说明
地图引擎 Leaflet 1.9.4(本地化) 体积小、插件生态够用,静态托管即可
底图 纸质方格层 + 地形瓦片 瓦片失败自动降级,不依赖 API KEY
标绘 自研 milplot.js(22 KB) 箭头、阻击阵地、封锁线、合围圈、军标点符
投影 EPSG:3857(Web 墨卡托) 几何在米制平面计算,线宽按屏幕像素
交互 原生 JS + CSS 动画 时间轴联动、自动巡展、新要素闪烁
部署 静态托管(描述一句即可发布) 一个链接分享,无需服务端

选型上唯一的硬要求是必须能离线运行:所有依赖本地化,瓦片取不到就退成方格纸底,页面不会白屏。

三、实现过程

3.1 底图与投影

纸质方格层永久垫底、地形瓦片叠加,瓦片取不到时自动降级,页面不会白屏。标绘几何在 EPSG:3857 米制平面计算,线宽却按屏幕像素定义、缩放后再重算,箭头在任意比例尺下都不变形。

js 复制代码
// 屏幕像素 → 米制长度(随纬度、缩放变化)
function metersPerPixel(zoom, lat) {
  return 156543.03392 * Math.cos(lat * Math.PI / 180) / Math.pow(2, zoom);
}
// 线宽按像素定义,换算成米制偏移后再生成几何
function widthInMeters(px) {
  return px * metersPerPixel(map.getZoom(), map.getCenter().lat);
}
map.on('zoomend', () => engine.render()); // 缩放后必须重算全部几何

这一条是标绘类项目的地基:几何用米、观感用像素。只要想清楚这句话,缩放时箭头变胖、虚线段忽长忽短的问题就不会出现。

3.2 标绘符号

没有可用的符号库,WorkBuddy 便写了一版:样条加法向偏移生成锥形箭杆与倒钩箭镞,另有阻击阵地锯齿、封锁线双边齿、合围圈闭合虚填。

要素类型 视觉规则 用途
arrow 样条中轴 → 法向偏移出左右边 → 沿路径收窄 → 倒钩 行军、进攻方向
blockade 双边齿线 封锁线、防线
ring 闭合样条 + 虚线填充 合围、包围圈
unit / place divIcon + 内联 SVG 部队标号、地名
note 纯文本,低倍率自动隐藏 箭头旁说明
js 复制代码
// 由路径点生成锥形箭杆 + 倒钩(伪代码,保留思路)
const spine = catmullRom(points, 24);        // 样条平滑
const left  = offset(spine, +width / 2);     // 法向偏移
const right = offset(spine, -width / 2);     // 反向偏移
const tip   = barb(head, angle, len);        // 倒钩

两个让"看起来像"的细节:一是箭杆沿路径逐渐收窄 ,比等宽带更像军事标图;二是箭镞用两侧倒钩而不是三角,远看才有锋。

【图 2|系统运行图】 局部放大 2 倍:一支进攻箭头的锥形箭杆与倒钩箭镞、一条带双边齿的封锁线、一个闭合虚填的合围圈同框。

3.3 数据与时态

十个时间节点的态势收敛成一份数据文件,图层分地理底、标绘、注记三层。切换节点时,前序态势降低透明度留作底色,且只留线条、隐去文字------既看得见来路,又不至于糊成一团。

js 复制代码
// 一个节点就是一份"时态切片"
{ view:  { center: [25.72, 110.92], zoom: 11 },
  story: ['...', '...', '...'],
  defs:  [{ kind: 'arrow',    pts: [...], side: 'red',  phases: [3, 4] },
          { kind: 'blockade', pts: [...], side: 'blue', label: '第四道封锁线' }] }

透明度不是拍脑袋定的,而是一组明确规则:

要素状态 opacity 说明
本节点出现 1 全亮
历史节点(延续) 0.11 只留线状要素,点状军标直接 0,避免文字堆叠
低倍率(zoom < 8.5)的 note 0 箭头旁说明会互相压住,放大后自动恢复
图层关闭 / 低于 minZoom 0 统一出口,便于调试

关键设计是 phases 数组:一个要素属于哪几幕,决定了它"什么时候出现、什么时候变成底色"。判断某要素是否本幕新增,只看上一幕的 phases 是否已含它------这样闪烁强调才只闪新东西,而不是整张图一起闪。

js 复制代码
if (ph.indexOf(state.phase) >= 0) return 1;                 // 本幕
if (state.historyFade && ph.some(p => p < state.phase)) {   // 历史
  return MARKER_KINDS.includes(def.kind) ? 0 : 0.11;
}
return 0;                                                    // 未到

3.4 移动端取景算法(本文最值钱的一段)

桌面视野原样搬到 390px 手机上,会裁掉大半标绘。WorkBuddy 没有用缩放敷衍,而是量出本节点标绘要素的包围盒重新 fit,再把画面整体上移避开底部控制条。

js 复制代码
function viewFor(i) {
  const v = PH[i].view;
  if (!isMobileLayout()) return { center: v.center, zoom: v.zoom };

  const b = phaseBounds(i);                    // 只统计本节点标绘,排除地名底图
  if (!b || !b.isValid()) return { center: v.center, zoom: Math.max(4, v.zoom - 1.25) };

  let z = map.getBoundsZoom(b, false, L.point(166, 138)); // 边距:横向多留,部队标号是定宽文字块
  z = Math.min(v.zoom, Math.max(4, z));        // 不放大过桌面设计倍率

  const pt = map.project(b.getCenter(), z);
  pt.y += 54;                                  // 内容上移,避开底部控制条
  return { center: map.unproject(pt, z), zoom: z };
}

三个参数值得抄走:

  • 边距 166 × 138 不是均分的,横向更大。因为部队标号是屏幕定宽的 DOM 节点,地图缩小时它不缩,横向不留白就会被裁。
  • Math.min(v.zoom, z) ------ fit 出来的倍率只能更小,不能更大。否则小范围节点会被放到过近,丢失周边地形参照。
  • pt.y += 54 在投影坐标里平移,而不是给容器加 padding。移动端底部有标题条和播放条,直接平移视野最省事。

【图 3|系统运行图】 手机竖屏,同一节点在手机上完整入画(对照桌面视野被裁掉的部分)。


【图位4|系统运行图】 点击底部标题条后,时间轴以抽屉形式上滑,占屏高约 80%,地图被遮罩压暗。

3.5 验证与发布

WorkBuddy 先用无头截图逐节点核对取景与标注重叠,再直连真实浏览器、以合成触摸事件测交互。发布后自动播放不生效,它去比对线上文件的 md5 与字节数,查出 CDN 缓存了旧版脚本,加版本号即恢复。

这一节的方法论可以单独拿走:别靠眼睛验收。包围盒、md5、合成触摸事件都是可编程的判据,比"我瞅着还行"可靠得多。

3.6 部署:一句话就能上线

这部分我没有写一行部署脚本,也没有碰服务器。想给同事看的时候,我只说了一句"发布到线上,给我一个能转发的链接",WorkBuddy 就把静态目录打包上传、拉起托管、回一条可访问地址;之后每次改完再说"同步到线上",它覆盖更新,链接保持不变 ,已经发出去的地址不用重发,深链 #p=5 也照样有效。

换句话说,从改代码到别人能打开,中间不需要自己配服务器、域名、HTTPS 或 CI。如果需要把成果发布为网页供其他人访问,直接跟 WorkBuddy 描述需求即可,它会自动执行------描述得越具体(发布到线上 / 更新已有链接 / 给个分享链接),它做得越准。

一个细节值得注意:覆盖发布后,浏览器里看到的可能仍是旧页面。原因不是没发成功,而是 CDN 边缘缓存(见踩坑第 1 条)。所以我在验证时先 md5sum 比对线上与本地文件,确认内容一致,再判断"功能有没有生效"。

四、实现效果

成品是一张可分享的网页,地图、时间轴、正文三栏联动。进页自动巡展,约七秒一步,播到遵义会议停止;切换节点时,新出现的箭矢与部队标号会闪烁一次,重点自己跳出来。

手机端同样能用:地图铺满全屏,时间轴收成底部抽屉,桌面视野会裁掉的标绘改为按包围盒重新取景。背景音乐循环播放,断网也能打开。

线上复验通过:巡展正常推进、无脚本报错、触摸交互可用。链接可直接转发,深链能直达任意一幕。

【图5|系统运行图】 第 5 幕切换瞬间:新增的三处战斗标记带准星环、新箭矢带辉光,其余要素保持常态(用于说明"只闪新要素")。

五、踩坑记录(现象 / 原因 / 解法)

  1. 改完 JS 线上没反应。 现象:本地正常,线上仍是旧行为。原因:CDN 边缘缓存了同 URL 的旧文件(13206 字节 vs 17765 字节,md5 不同)。解法:所有本地资源加 ?v= 版本号,每次改动递增;排查时用 md5sum 比对线上与本地,而不是猜。
  2. 手机地图整块点不动。 现象:所有触摸无响应。原因:全屏遮罩虽 opacity:0,但仍占满视口并接管点击。解法:关闭态加 pointer-events:none,打开时再恢复。
  3. 准星环缩成一条竖线。 现象:点状军标的强调环宽度只剩 8px。原因:divIcon 容器盒宽度为 0,内容靠 transform 溢出对位。解法:用 Range.selectNodeContents 量内容包围盒,再把环的 left/top/width/height 写死。
  4. 动画"没生效"。 现象:无头截图里看不到闪烁。原因:虚拟时钟下 CSS 过渡不推进,截图恒停在同一帧。解法:动效验证改用真实浏览器实时采样,或用 animation-play-state:paused 冻在波峰再截。
  5. 缩小时标签互相压住。 现象:跨度大的节点上十几个部队标号叠成一团。原因:文字是屏幕定宽,地图缩小而文字不缩。解法:做插值分级------低倍率隐去 note 与标号副名,放大自动恢复;同时压低历史要素不透明度。
  6. 深链被自动播放覆盖。 现象:#p=5 打开后 1.6 秒被巡展拉回第 1 幕。原因:自动起播的看门狗只看"当前是否在播放",不看用户意图。解法:有深链时置 deepLink 标记并跳过自动起播。

六、体积与性能

资源 体积 首屏加载
leaflet.js 147 KB 是
style.css 35 KB 是
app.js 26 KB 是
milplot.js 22 KB 是
scenario.js 22 KB 是
music.mp3 3.2 MB 否(首次手势后按需)

首屏只需约 260 KB 代码与样式,3.2 MB 音频延后加载------这就是"断网也能打开"却仍带背景音乐的原因:地图与史料不依赖网络,音频失败只是少一层氛围。

七、总结

历史 GIS 可视化里,"好看"和"对"是两件事。这次最值得记的三条经验:术语先对齐再动手("军事标绘"没有唯一定义,先问一句比写完推翻划算);能自动验证的别靠眼估(包围盒、md5、触摸事件都是可编程判据);静态资源一律带版本号。从一句含糊需求到一条能发出去的链接,中间没有返工。

在线体验:在线访问地址静态站点部署,深链 #p=5 可直达任一幕。图中坐标为概略示意位置,不作测绘依据。

相关推荐
七夜zippoe1 天前
TextIn xParse 实战:用版面坐标做面单隐私取证与脱敏
脱敏·textln·workbuddy·ai数据层基础设施
一个天蝎座 白勺 程序猿2 天前
AiiOnly + WorkBuddy 搓了个文本对比小工具:选型、接入、成本一次讲清
ai·ai集成·ai使用·workbuddy·aiionly·文本对比
fensnote2 天前
一句话,让 AI 把一块 ESP8266 从头调到能上网
ai·esp8266·nodemcu·workbuddy
stereohomology2 天前
Kimi K3.1 Pro试用:可称之曰能
人工智能·workbuddy
feasibility.4 天前
不装Claude Code,WorkBuddy也能做视频,Remotion skill告别抽卡(含安装与使用)
人工智能·aigc·音视频·agi·skill·remotion·workbuddy
Xxtaoaooo5 天前
双11避坑:我用 WorkBuddy + TextIn xParse 做了个「避坑对比台」,商品详情图丢进去,参数矛盾自动现形
人工智能·1024程序员节·textin·workbuddy·ai数据层基础设施
xiezhr7 天前
你的WorkBuddy不好用,大概率是这些配置没设置好
ai·ai agent·国产ai·workbuddy
Zhu75810 天前
白嫖work Buddy+腾讯CNB,制作日报周报汇报工具
workbuddy
iReachers13 天前
workbuddy和豆包网页文件打包成exe程序指南
豆包·workbuddy·html打包exe·网页转exe·打包教程