跳到主要内容

Travel 相册与地图功能实现说明

这篇文档不是给“只想加一篇新游记”的人看的操作手册,而是给以后继续维护这套功能的人看的实现说明。

目标只有一个:

  • 把当前 Travel 板块为什么这样设计、现在是怎么跑起来的、以后应该沿着哪条线继续维护,讲清楚。

如果你只想新增内容,先看:

如果你遇到高德点位整体侧漂,先看:

这套功能最终解决了什么问题

当前 Travel 板块不是单纯的“图片排版”,而是一条完整链路,解决了下面几件事:

  1. 把一篇旅行内容稳定落成 docs/Travel/*.mdx 页面。
  2. 把图片引用统一规范成可维护的相对路径,而不是散落的完整 OSS URL。
  3. 自动从 JPEG 提取 EXIF、拍摄时间、设备、镜头、GPS。
  4. 对有 GPS 的图片补高德逆地理编码,得到更像“人话”的地点名。
  5. 在文章页展示“这篇文章的拍摄点地图”。
  6. 在 Travel 分类首页展示“我的足迹”总览、足迹地图和旅行文章卡片。
  7. 让以后新增文章时,不需要重新手写一遍地图、卡片和元数据逻辑。

设计原则

这套实现背后有五条固定原则:

1. 文章文件负责表达内容结构

docs/Travel/*.mdx 决定:

  • 文章标题
  • 段落顺序
  • 图片分组
  • 每组图片出现的顺序

也就是说,文章文件是“叙事结构”的唯一真源。

2. 元数据和地图数据都走生成产物

前端不应该每次运行时重新去读 EXIF、重新请求逆地理编码,也不应该自己临时推断地图结构。

所以现在有两份生成产物:

  • src/data/travelPhotoMetadata.generated.json
  • src/data/travelMap.generated.json

前者解决“单张图片的信息”,后者解决“文章级与地点级地图信息”。

3. 原始 GPS 保留在数据层

照片 EXIF 里的坐标保留原始 GPS,不在生成阶段强行改成高德可直接渲染的坐标。

这样做的原因是:

  • 原始数据可追溯
  • 逆地理编码和别的地图 SDK 仍然可以复用原始 GPS
  • 坐标系转换集中在前端地图渲染层,职责更清晰

4. 地图按页面角色决定加载方式

文章页地图默认折叠,用户点击后再展开;Travel 分类首页的地图是页面核心信息之一,所以以嵌入模式直接渲染。

这里不是互相矛盾,而是页面角色不同:

  • 文章页里,地图是正文后的辅助信息,不能抢正文注意力。
  • Travel 首页里,地图和“我的足迹”总览是首屏主内容,可以直接加载。
  • 两种模式复用同一个 TravelOverviewMap / TravelMap 底层能力,只是初始化时机不同。

5. 生成流程优先可维护,不优先“一次性最快”

所以现在不是直接从“用户口头描述”一步到最终 MDX,而是更偏两阶段:

  1. 先整理成源稿
  2. 再由脚本生成最终文章

这比一次性直接手写最终 MDX 更稳,因为以后改文案、增删图片、重排分组时,可以复用同一条链路。

总体分层

当前实现可以拆成四层。

层级主要职责关键文件
内容层保存最终 Travel 文章docs/Travel/*.mdx
生成层生成文章、照片元数据、地图数据scripts/create-travel-article.mjsscripts/generate-travel-photo-metadata.mjsscripts/generate-travel-map-data.mjs
数据层给前端提供稳定 JSONsrc/data/travelPhotoMetadata.generated.jsonsrc/data/travelMap.generated.json
展示层渲染相册、灯箱、文章地图、Travel 首页、总览地图src/components/TravelGallery/*src/components/TravelMap/*src/components/TravelHome/*

端到端数据流

完整流程是这样跑的:

  1. 用户给出地点、OSS 图片链接、少量描述,或者直接给一份 Markdown 原稿。
  2. AI 先整理出源稿,或者你手动准备好源稿。
  3. scripts/create-travel-article.mjs 把源稿转成 docs/Travel/<slug>.mdx
  4. scripts/generate-travel-photo-metadata.mjs 扫描所有 Travel 文章里的图片引用,下载 JPEG,提取 EXIF 和 GPS,并在需要时调用高德逆地理编码。
  5. scripts/generate-travel-map-data.mjs 再根据文章里的 TravelGallery 顺序和照片元数据,生成文章页地图与分类页地图需要的结构。
  6. TravelGallery 从照片元数据 JSON 里补齐每张图的展示信息。
  7. TravelHome 从地图 JSON 里读取文章、地点和照片数据,渲染 Travel 分类首页。
  8. TravelStoryMapTravelOverviewMap 从地图 JSON 里读点位,再在前端渲染高德地图。

内容层:为什么最终文章仍然是 MDX

最终落地在 docs/Travel/*.mdx,而不是只保留数据库或 JSON,原因有三个:

  1. Docusaurus 原生就以文档文件为核心。
  2. 文章正文和图片组的关系,本质上就是文档结构。
  3. MDX 可以直接复用 TravelGalleryTravelStoryMap 组件,不需要额外解释层。

当前一篇 Travel 文章至少包含:

  • front matter
  • TravelGallery 引入
  • TravelStoryMap 引入
  • 若干 createTravelPhotos([...])
  • 正文与图片块交替出现

Travel 文章的 front matter 还承担分类页排序职责。当前约定是用旅行开始日期写 sidebar_position: -YYYYMMDD,例如 sidebar_position: -20260207。Docusaurus 的 autogenerated sidebar 会按 sidebar_position 升序排列,因此日期越新,负数越小,文章越靠前。这样 Travel 侧栏、上一篇/下一篇导航和 Travel 首页卡片可以保持同一套“最新旅行在前”的顺序。

不要用 1, 2, 3... 表示新旧顺序。那种写法每新增一篇更新的旅行文章都要整体改号,维护成本会越来越高。也不要为了排序重命名 Travel 文件;当前地图数据和首页展示仍把文件名 basename 作为文章 id,重命名会影响卡片封面、卡片描述和生成数据的稳定关联。

文章生成层:为什么单独做 create-travel-article

关键脚本:

  • scripts/create-travel-article.mjs

它存在的目的不是“偷懒”,而是把格式规范固化下来。

这个脚本现在负责:

  1. 解析 front matter。
  2. 容忍 UTF-8 BOM。
  3. 识别 Markdown 图片行。
  4. 只接受 oss.nevergpdzy.com 下的 JPEG,或者已经归一化好的 JPEG 相对路径。
  5. 把完整 OSS URL 转成仓库内统一使用的相对路径;如果只写文件名,会按旧规则归到 img_for_Typora/
  6. 保留 ## 章节结构。
  7. 自动生成 createTravelPhotos([...])
  8. 自动插入 <TravelStoryMap />
  9. 在需要时继续联动 generate:travelbuild

CLI 参数的当前行为是:

  • --source / -s 指向源稿,必填。
  • --sync 在生成 MDX 后运行 npm run generate:travel
  • --verify 在生成 MDX 后运行 npm run generate:travelnpm run build
  • --force 允许覆盖已存在的目标文件;没有这个参数时,脚本会拒绝覆盖。
  • 源稿 front matter 的 output 只用于指定输出路径,不会写进最终 MDX。

这一层的核心思路是:

  • 把“格式规范”写进脚本,而不是写进人脑。

这样以后无论是你自己手工补文章,还是 AI 帮你补文章,最终落到仓库里的结构都会尽量一致。

照片元数据生成层:为什么不在前端读 EXIF

关键脚本:

  • scripts/generate-travel-photo-metadata.mjs

前端不适合直接处理 EXIF,有几个原因:

  1. 图片体积太大。
  2. 浏览器端解析不稳定且浪费性能。
  3. 逆地理编码需要请求节流和缓存。
  4. 构建后静态站点更适合消费现成 JSON。

所以现在的做法是:

  1. 扫描 docs/Travel/*.mdx 里出现的 JPEG。
  2. 统一把引用归一化为 canonical image key。
  3. 下载图片。
  4. 从 JPEG 中解析 DateTimeOriginalModelLensModel、GPS。
  5. 如果有 GPS,再调用高德 Web 服务逆地理编码。
  6. 生成 travelPhotoMetadata.generated.json

这一层还做了几件维护性很重要的小事:

  • 复用已有缓存,不重复请求
  • 对高德请求做节流,避免无限打 API
  • 对 iPhone 镜头名称做归一化
  • 当 GPS 不可用时,允许回退到文章标题继承的地点名

地图数据生成层:为什么还要多一层 travelMap.generated.json

关键脚本:

  • scripts/generate-travel-map-data.mjs

看上去前端已经有照片元数据了,似乎可以直接拿来画地图,但当前项目没有这么做。原因是前端还缺几类“文章级信息”:

  • 这篇文章对应哪个 permalink
  • 各个 TravelGallery 的顺序是什么
  • 哪张图属于哪个章节
  • 哪个点适合作为文章概览点
  • Travel 分类页应该用哪个点来代表“去过这个地方”

所以这层脚本单独把“适合地图渲染”的结构整理好,再交给前端消费。

这层的几个关键设计决定是:

1. 地图只展示点,不展示路线

当前 Travel 地图不再画路线,只展示:

  • 文章页中的拍摄点
  • 分类页中的地点代表点

原因是实际效果上,照片 GPS 更适合表达“拍到了哪里”,不适合表达“完整怎么走的路线”。

2. 分类页用代表点,而不是所有点硬铺开

分类页如果把一篇文章的全部点都展开,信息量会太大,也很难点击。

所以脚本会为每篇文章求一个较合适的代表点,再汇总成总览地图。

3. 章节顺序继续从文章里继承

地图数据不是独立定义章节,而是回读文章里的 TravelGallery 与标题顺序。

这能保证地图和正文不会各说各话。

展示层:为什么拆成 TravelGalleryTravelMap

TravelHome

关键文件:

  • src/components/TravelHome/index.js
  • src/components/TravelHome/styles.module.css

这一层负责 Travel 分类首页,而不是文章详情页。

它现在做四件事:

  1. 渲染顶部“我的足迹”总览卡。
  2. 渲染嵌入式足迹地图。
  3. 渲染旅行文章卡片列表。
  4. 在桌面端渲染底部引用句,移动端隐藏这句,避免页尾显得拥挤。

TravelHome 的内容数据来源仍然是 src/data/travelMap.generated.json,不单独维护另一份旅行文章清单。卡片展示顺序则来自 Docusaurus 已经按 sidebar_position 排好的 Travel sidebar items。

具体规则是:

  • 地点数、文章数来自 travelMapData.placeCount / articleCount
  • 旅行文章卡片内容来自 travelMapData.articles
  • 旅行文章卡片顺序来自 Travel category 的 sidebarItems,保持和左侧侧栏、上一篇/下一篇导航一致。
  • 如果某篇文章暂时没有出现在 sidebarItems 里,TravelHome 会把它追加在已排序文章之后,避免因为排序数据缺失导致卡片消失。
  • 卡片封面优先读取 CARD_COVER_IMAGES 中按文章 id 配置的展示图;未配置时回退到文章里的第一张照片,并按 oss.nevergpdzy.com 的远程图片规则拼接。
  • 卡片描述使用 CARD_DESCRIPTIONS 中的展示文案,而不是直接复用 front matter 里的长描述。
  • 飞行、火车、自驾里程是当前 Travel 首页的人工展示数据,不从照片元数据推导。

CARD_COVER_IMAGES 只影响 Travel 首页卡片封面,不会修改 docs/Travel/*.mdx 的正文相册、文章描述或 src/data/travelMap.generated.json 的生成数据。 CARD_DESCRIPTIONS 只影响 Travel 首页卡片,不会修改 docs/Travel/*.mdx 的文章描述。

这里有一个容易误解的边界:travelMap.generated.json 的文章列表按文件名读取后生成,真正展示顺序由 TravelHome 再根据 Docusaurus 传入的 sidebarItems 重排。也就是说,新增文章时要先保证 sidebar_position 正确,再通过构建验证首页卡片、侧栏和上一篇/下一篇导航是否一致。

新增 Travel 文章时,必须在 front matter 写入旅行开始日期对应的负数排序值:

sidebar_position: -20260430

同一天多篇文章时,不要追加整数位,例如不要写 -2026043001,因为它会比普通日期小很多,长期压到更前面。需要同日微调时使用小数后缀:

sidebar_position: -20260430.02
sidebar_position: -20260430.01

小数后缀只在同一天附近调整顺序,不会越过相邻日期。

Travel 首页视觉结构

Travel 首页不是 Docusaurus 默认 generated index 的简单换皮,而是一个专门的首页视图。

当前结构是:

  1. 左侧“我的足迹”总览卡。
  2. 右侧嵌入式地图卡。
  3. 下方旅行故事卡片网格。
  4. 桌面端底部中文引号引用句。

维护时要注意几个边界:

  • 左侧总览卡背景固定使用 https://oss.nevergpdzy.com/AI-images/tranquil-valley-path.png
  • 统计区固定为两行:第一行是“探索地点 / 旅行故事”,第二行是“飞行 / 火车 / 自驾”。
  • 统计项要左对齐,文字在图标右侧左对齐,图标在自己的浅色方块内垂直居中。
  • 右侧地图卡不再显示额外标题栏和“7 个地点”,这样地图显示面积更大。
  • 旅行文章卡片不显示日期和阅读量,只显示地点胶囊、封面、标题和短描述。
  • 底部引用句桌面端用中文全角引号装饰;手机端隐藏。

这个页面的视觉目标是“旅行总览”,不是营销 landing page,也不是普通文档目录页。因此不要把默认 DocCardList 的列表样式重新塞回来。

TravelGallery

关键文件:

  • src/components/TravelGallery/index.js

这一层负责:

  • 归一化图片引用
  • 拼接远程图片地址
  • travelPhotoMetadata.generated.json 中补齐拍摄时间、设备、镜头、地点
  • 渲染网格相册和灯箱

它不负责:

  • 决定文章结构
  • 请求高德地图
  • 解析 EXIF

TravelMap

关键文件:

  • src/components/TravelMap/shared.js
  • src/components/TravelMap/TravelStoryMap.js
  • src/components/TravelMap/TravelOverviewMap.js
  • src/components/TravelMap/TravelMapDisclosure.js

这里又分成三部分:

  1. shared.js 负责高德配置读取、loader 单例、地图创建、控件接入、坐标转换、fit view、销毁与 resize。
  2. TravelStoryMap.js 负责文章页折叠地图、照片点聚合、marker、点击信息窗。
  3. TravelOverviewMap.js 负责地点概览图和跳转文章;默认折叠,也支持 Travel 首页使用的 embedded 嵌入模式。

TravelOverviewMap 的两个模式要区分清楚:

  • 默认模式:走 TravelMapDisclosure,点击后才创建 inline map,适合非首屏辅助地图。
  • embedded 模式:组件挂载后直接创建地图,适合 Travel 首页右侧的首屏足迹地图。

两个模式共享同一套地点数据、坐标转换、marker 创建、全屏弹窗和失败 fallback。

为什么 shared.js 要做成公共层

因为地图真正稳定运行,依赖的是一套共用规则:

  • 统一读取 key / serviceHost / securityJsCode
  • 统一设置 window._AMapSecurityConfig
  • 统一懒加载 @amap/amap-jsapi-loader
  • 统一根据设备能力决定是否加载 AMap.ToolBarAMap.Scale
  • 统一收紧地图渲染参数,例如默认走 2D、关闭旋转 / 俯仰 / 3D 楼块,只保留必要底图要素
  • 统一做 gps -> 高德坐标 转换
  • 统一 fitTravelMapToOverlays
  • 统一在组件折叠或卸载时 map.destroy()

只要这些逻辑散落到各个地图组件里,后面就一定会出现行为漂移。

高德接入层:为什么现在同时支持 serviceHostsecurityJsCode

关键文件:

  • docusaurus.config.js
  • src/components/TravelMap/shared.js

这部分的核心矛盾是:

  • 前端加载高德 JSAPI 需要 AMAP_JSAPI_KEY
  • AMAP_SECURITY_JS_CODE 不应该在生产环境里长期裸露给客户端

所以现在的策略是:

  1. 本地或无代理环境下,可以用 securityJsCode 兜底。
  2. 生产环境优先走 AMAP_SERVICE_HOST
  3. 一旦配置了 AMAP_SERVICE_HOST,构建时就不再把 AMAP_SECURITY_JS_CODE 注入前端 customFields.amap

具体逻辑在 docusaurus.config.jsAMAP_SERVICE_HOST 有值时,customFields.amap.securityJsCode 会被写成空字符串;否则才读取 AMAP_SECURITY_JS_CODE 或 fallback 值。前端统一通过 src/utils/amap.js 设置 window._AMapSecurityConfig

这样做的好处是:

  • 本地调试仍然简单
  • 正式上线时可以把真正的安全密钥留在服务器代理层

坐标处理:为什么选择“渲染前转换”,而不是“生成时覆盖”

这件事单独重要到值得再写一遍。

当前数据里保存的是原始 EXIF GPS,这意味着:

  • travelPhotoMetadata.generated.json 里的坐标仍然是原始 GPS
  • travelMap.generated.json 里的点位也继续沿用这组原始 GPS

真正变成高德可直接渲染坐标,是在前端调用:

AMap.convertFrom(..., 'gps')

这个设计是故意的,不是疏忽。原因是:

  1. 原始 GPS 更通用。
  2. 生成脚本不应该偷偷写入“只适合某个地图厂商”的坐标。
  3. 地图厂商切换时,只需要换渲染层,不需要重洗历史数据。

相关排障记录见:

地图交互:为什么文章页折叠、首页直接渲染

现在地图有两种交互模式:

  1. 文章页拍摄点地图:通过 TravelMapDisclosure 折叠。
  2. Travel 首页足迹地图:通过 TravelOverviewMap embedded 直接渲染。

文章页继续折叠,原因有三条:

  1. 地图不应该压过正文。
  2. 高德 JSAPI 加载成本不低。
  3. 折叠后才初始化地图,可以减少无意义的 WebGL 实例。

Travel 首页直接渲染,原因也很明确:

  1. 足迹地图是首页首屏信息的一部分,不是正文后的补充组件。
  2. 用户进入 Travel 首页时,预期就是先看到“去过哪里”。
  3. 地图外层已经被压成紧凑卡片,不会像文章页大地图那样打断阅读。

当前行为是:

  • TravelStoryMap 默认不展开,用户点击后才真正创建地图。
  • TravelOverviewMap 默认仍可折叠,保持组件通用性。
  • TravelHome 调用 TravelOverviewMap embedded,挂载后直接创建地图。
  • 折叠模式下收起面板时销毁 inline map,避免隐藏状态继续占着 WebGL 资源。
  • 嵌入模式下监听窗口尺寸变化,主动请求地图 resize,避免响应式布局后画布尺寸不对。
  • 页面销毁时销毁 map 实例。

Travel 首页响应式规则

Travel 首页的响应式不是简单缩放桌面端,而是按设备重新分配注意力。

桌面端

  • 顶部是左右双列:左边“我的足迹”更宽,右边地图更窄。
  • 左右卡片高度保持一致,地图不能因为内部 canvas 撑高整行。
  • 统计项固定两行靠左,不拉伸填满整行。
  • 文章卡片优先多列展示,避免单张卡片过大。
  • 底部引用句显示,使用中文全角引号装饰。

桌面宽屏与侧栏规则

Travel 首页在桌面端应该被当作独立展示页,而不是普通 docs 文章目录页。维护时要同时保证两个入口都走无侧栏宽屏布局:

  • /docs/Travel
  • /docs/category/travel

这两个入口都应该由 src/utils/travelDocs.js 里的 isTravelDocPath 识别为 Travel 页面,然后在 DocRoot/Layout 层跳过 DocRootLayoutSidebar,并让 DocRoot/Layout/Main 使用 Travel 专用的宽屏容器。

这样做的原因是:

  • 左侧 Docusaurus sidebar 会挤压首屏展示区,让“我的足迹”和地图卡片变成窄卡。
  • Travel 首页顶部两张卡片需要保持稳定的左右比例,桌面宽屏下约为 2:1
  • 下方旅行故事卡片在桌面宽屏下应保持每行四个,展示区整体居中,卡片随可用宽度等比例变大。

如果以后调整 Travel 首页路由或 Docusaurus generated index 配置,必须同步检查 src/utils/travelDocs.js 里的 Travel 路径判断,否则很容易出现某个入口仍然带侧栏、另一个入口正常的割裂状态。

移动端

  • 顶部总览卡尽量压低,避免首屏被一个 hero 占满。
  • 统计项仍是两行靠左,但每个 chip 只按内容和最小宽度占位。
  • 地图高度单独压低,只作为“位置感”入口,不做大面积地图浏览。
  • 文章卡片一列展示,但图片比例要收紧,避免每张卡都像大海报。
  • 底部引用句隐藏,避免和 footer 挤在一起。

维护时不要把移动端直接回退成桌面布局的缩放版;这里更重要的是扫描效率和手感。

移动端交互性能优化:为什么现在要刻意“减配”

这部分是后续维护时很容易被“顺手加回去”的地方,所以单独记一下。

项目上线后,移动端最直接的问题不是“点位太多”,而是滑动、拖拽、双指缩放时的每一帧成本偏高。

当时主要有三个风险点:

  1. 地图曾经出现过在不合适页面首屏立即初始化的回退行为。
  2. 地图实例虽然没有使用俯仰、旋转或 3D 楼块效果,但初始化时仍然按 3D 模式创建。
  3. 移动端仍然加载了 ToolBarScale 这类更偏桌面操作的控件。

现在的处理原则是:

  • Travel 地图优先保证移动端交互顺滑,而不是追求“参数看起来开得很满”。
  • 只要当前页面没有明确使用 3D 视角,就不要为了“理论上更强”而保留 3D 开销。
  • 隐藏状态的地图实例不保活,折叠后直接销毁,重新展开再创建。
  • Travel 首页这种明确需要首屏地图的场景,可以使用嵌入模式,但必须控制地图尺寸。

具体落地成了下面几条规则:

  • shared.js 创建地图时默认使用 2D 模式。
  • 统一关闭 pitchEnablerotateEnableshowBuildingBlockshowIndoorMap
  • 地图底图只保留 bg / road / point,不额外打开这套页面不需要的更重图层元素。
  • 对粗指针设备(手机、平板一类)默认不加载 AMap.ToolBarAMap.Scale
  • TravelOverviewMap 默认模式保持折叠,只有 embedded 模式会自动初始化。
  • 文章页 inline map 在折叠时会立刻 destroy()
  • Travel 首页嵌入地图不显示额外标题栏,减少 UI 遮挡,把空间留给地图本体。

这套收缩不是为了“阉割功能”,而是因为 Travel 地图的职责本来就是辅助阅读:

  • 能看清点位分布就够了
  • 不需要在这里展示 3D 城市效果
  • 不需要为了少量点位维持一个隐藏的活跃 WebGL 实例

如果以后这里真的演变成高密度点位或长时间交互场景,再考虑把覆盖物继续换成 LabelMarker 这类更偏性能的实现,而不是先把 3D 和桌面控件重新开回去。

这套实现为什么没有继续保留“路线”

这是一个明确的产品取舍,不是功能没做完。

之前路线的主要问题是:

  • 视觉上像轨迹图,不像相册
  • 多数旅行图组的 GPS 点并不适合线性讲述
  • 有些照片没有 GPS,路线天然不完整
  • 地图越像“导航产品”,正文越容易被抢焦点

所以这次改成:

  • 文章页只标拍摄点
  • 分类页只标去过的地方
  • 点位说明仍然来源于照片元数据和文章结构

这条线更贴合这个站点的 Travel 板块定位。

为什么这套实现适合你现在的工作方式

你现在最常见的输入,不是直接提交一篇完整 MDX,而是:

  1. 一个地点
  2. 一组 OSS 图片链接
  3. 一点点你自己的感觉和风格要求

所以当前系统刻意兼容这种输入方式:

  • 人可以先只提供少量描述
  • AI 先补源稿
  • 再自动生成文章
  • 再自动生成元数据和地图

这比要求你先把每篇旅行都手写成完全规范的 MDX 更符合实际。

以后继续扩展时,优先遵守的规则

1. 不要手改生成 JSON

下面这两份文件都视为生成产物:

  • src/data/travelPhotoMetadata.generated.json
  • src/data/travelMap.generated.json

要改逻辑,就改脚本,不要直接修 JSON。

2. 新增地图组件时先复用 shared.js

不要在新组件里重新写:

  • 高德 loader 初始化
  • 安全配置
  • 坐标转换
  • fit view
  • 销毁逻辑

3. 新增内容优先走“源稿 -> 最终 MDX”链路

不要回退到每次都手工拼 createTravelPhotos([...])

4. 生产环境优先走代理

不要默认把 AMAP_SECURITY_JS_CODE 跟着静态构建一起公开发布。

5. 不要把路线逻辑重新塞回来

除非 Travel 板块的产品目标真的变了,否则不要让地图从“拍摄点概览”重新变回“导航路线图”。

当前最常用的命令

npm run create:travel-article -- --source drafts/travel/<slug>-source.md
npm run sync:travel-article -- --source drafts/travel/<slug>-source.md
npm run create:travel-article -- --source drafts/travel/<slug>-source.md --verify
npm run generate:travel
npm run verify:travel

这几条命令分别对应:

  1. 生成文章
  2. 生成文章并同步刷新元数据与地图
  3. 生成文章、刷新元数据与地图,并构建验证
  4. 只刷新元数据与地图
  5. 刷新并构建验证

以后看这套实现,先从哪几个文件开始

如果以后你自己或别人要继续维护这套功能,建议阅读顺序是:

  1. docs/LabNotes/travel-photo-metadata-workflow.md
  2. scripts/create-travel-article.mjs
  3. scripts/generate-travel-photo-metadata.mjs
  4. scripts/generate-travel-map-data.mjs
  5. src/components/TravelGallery/index.js
  6. src/components/TravelHome/index.js
  7. src/components/TravelHome/styles.module.css
  8. src/components/TravelMap/shared.js
  9. src/components/TravelMap/TravelStoryMap.js
  10. src/components/TravelMap/TravelOverviewMap.js

按这个顺序读,最容易先看懂数据是怎么流的,再看懂前端为什么这样渲染。

一句话总结

当前 Travel 功能的核心不是“接了一个高德地图”,而是:

用文章结构做真源,用脚本生成元数据和地图数据,用前端只负责展示,从而把“旅行图片 + 文案 + 地图”变成一条可以重复执行、可以长期维护的内容生产链路。