1. 截图救不了复刻:GLM-5.1 复刻原神地图实录——Vue2+Leaflet 配置与验证
先说结论:截图对比、outerHTML 扒结构、F12 抄 CSS 这套"前端复刻三件套",在静态落地页上确实好用,但遇到原神大世界交互地图这种项目会直接失效。原因不复杂——这个页面根本不是"一张图",而是 10 万+ 动态标记点、自定义像素坐标系、Vue2+Quasar+Leaflet 深度耦合、服务端按需查询的组合体。你截一张图,AI 能给你生成一个"看起来像"的页面,但标记点位置全偏、弹窗不跟随、聚合失效、进度条永远 0%。
我这次用 GLM-5.1 走了一遍完整流程:从截图识别到瓦片坐标对齐,从 deck.gl 误判到 Leaflet 重写,从 Python 数据管道丢字段到前端表现异常。踩过的坑里最有代表性的是:一个 30 个字符的import 'leaflet/dist/leaflet.css'让模型卡了 15 分钟,以及 Python 转换脚本里 5 个 CRITICAL 数据损毁——脚本能跑、JSON 能生成、前端能加载,但所有功能都是错的。
这篇文章写给想用 AI 做地图类复刻的工程师。我会给出可复制的config.toml骨架、TaoToken 统一 Key/API 通道接入步骤、坐标校验与渲染验证动作,目标是把一个可交互地图原型跑通。适合谁:有 Vue2/Vue3 基础、想用 Leaflet 做自定义坐标系地图、或者正在被"AI 生成的代码能跑但视觉不对"折磨的人。
2. 原问题与场景:为什么截图三件套会失效
2.1 这个页面不是"一张图"
原神交互地图的核心复杂度在于它不是静态视觉,而是运行时行为。具体拆开看:
- 10 万+ 动态标记点,每个标记有自己的图标、状态、点击弹窗、所属物品分类
- 自定义坐标系,不是 WebMercator 经纬度,而是游戏像素坐标 + Leaflet
CRS.Simple投影 - Vue2 + Quasar + Leaflet 深度耦合,560 行单文件
IndexPage.vue、14 个 API 文件、魔改过的 markercluster 插件 - 服务端按需查询,每次操作向后端发 5-8 个 API 请求,过滤逻辑在 Java 后端
截图能捕获的只有"某一时刻某一缩放级别下的视觉快照",捕获不了标记点的数据绑定、坐标变换、图层切换、状态同步。这就是为什么abi/screenshot-to-code这类工具在静态页面上表现优秀,在动态地图上捕获率接近 0。
2.2 三方关系决定了架构边界
复刻之前必须理清数据来源和代码参考的关系,否则后面所有决策都会踩坑:
| 角色 | 做了什么 | 协议 |
|---|---|---|
| 空荧酒馆社区团队 | 原创地图数据维护,开源 v3 前端 | MulanPSL-2.0 |
| v3.yuanshen.site | 官方 Web 地图,复刻目标 | Vue2 + Quasar + Leaflet |
| 第三方独立开发者项目 | 完全独立重写,Preact + 自研 canvas 地图 | MIT,数据停在旧版本 |
关键结论:第三方项目和官方 v3 零代码共享。UI/交互参考官方开源前端,数据走官方公开 API。这个边界定清楚了,后面才不会在"要不要抄第三方实现"上浪费时间。
2.3 真实工程规模
很多人对"复刻一个网页"没概念,看几个数字就明白了:
| 数据 | 数量 | 说明 |
|---|---|---|
| 标记点 | 105,231 个 | 全地图每个宝箱、神瞳、特产的位置 |
| 物品分类 | 3,926 个 | 跨越 14 个类型 |
| 区域树 | 37 个 | 主区域 + 子区域 |
| 图标 | 1,087 个 PNG | 含中文文件名、哈希名、数字名三种命名 |
| 瓦片 | 12,750 张 | 四级缩放,带负坐标,约 300MB |
| data.json | 39 MB | 全量点位单体文件 |
这不是"做个组件"的工作量,是"搭一个数据管道 + 渲染引擎 + UI 系统"的小型工程。AI 要在这其中做对架构决策、翻译 Vue2 到 Vue3、写 Python 数据转换脚本三层工作,任何一层失误都会连锁影响下游。
3. TaoToken 前置:统一 Key/API 通道接入
3.1 为什么需要统一通道
复刻过程中会频繁调用模型做代码翻译、Bug 定位、数据字段分析。如果每个环节用不同的 Key 和 endpoint,排查问题时很难区分"是模型能力问题"还是"是通道配置问题"。用 TaoToken 做统一入口的好处是:一个 Key 覆盖模型对话、编码计划、控制台管理,接入文档和 API Keys 都在同一处管理。
官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
API 基础地址:https://taotoken.net/api
3.2 获取 Key 与配置
第一步,打开控制台创建 API Key:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console第二步,在 API Keys 页面复制你的 Key:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys第三步,把 Key 写进项目根目录的.env,不要提交到 Git:
# .env TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api3.3 config.toml 骨架
如果你用支持 TOML 配置的客户端或自建脚本,可以直接用下面这个骨架。注意base_url不要带 UTM 参数,API 调用只认纯地址:
# config.toml [provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" [model] default = "glm-5.1" max_tokens = 8192 temperature = 0.2 [project] # 复刻项目根目录 root = "/root/code/ssbh/capturegenshin" # 数据管道脚本 sync_script = "GenshenMap-python/map_sync.py" split_script = "GenshenMap-python/split_data.py" [map] # Leaflet 自定义坐标系配置 crs = "Simple" center_x = 3568 center_y = 6969 zoom_min = 10 zoom_max = 13 tile_size = 256注意:
center_y这个值很关键。我一开始用了源码里的 fallback 默认值 6286,结果标记点整体偏移 683 像素。真实运行时配置在webapp.json里,不是web-map.json。这个坑后面第 5 节会详细讲。
3.4 模型对话与 Coding Plan 分流
如果你只是验证模型对某段 Leaflet 配置的理解,用模型对话页面快速试:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat如果你要长期做编码、Agent 任务、多轮代码翻译,建议走 Coding Plan,额度更稳定:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan接入文档在这里,遇到参数问题先查文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc4. 可复制配置:Leaflet 集成与坐标对齐
4.1 第一次选型的教训:deck.gl 看起来很美
我最初让 GLM-5.1 做技术选型,它选了 deck.gl,理由是"WebGL 渲染,10 万+ 标记点无压力,正交投影比 Leaflet hack CRS 更自然"。这套方案在 Phase 0 和 Phase 1 确实顺利,瓦片能加载、标记能点击、弹窗能显示。
但到 Phase 1.5 开始崩。12 个意外踩坑接连出现,其中 5 个是架构级的:
| 能力 | Leaflet | deck.gl |
|---|---|---|
| 标记点点击弹窗 | layer.bindPopup自动定位 | 手动管理 DOM 弹窗 + 监听视图变化 |
| 标记点聚合 | L.markerClusterGroup一行配置 | 无原生聚合,需自行实现 |
| 图层独立管理 | 每物品独立 layer | 一个 ScatterplotLayer 管全部 |
| 标记完成状态 | layer.setIcon直接交换 DOM | 维护两份标记集合并切换 |
| CSS 控制显隐 | 修改 DOM class 即可 | 走数据层过滤或图层参数 |
核心问题:GLM-5.1 选 deck.gl 时只看"渲染性能"一个维度,没看"生态完整度"。在重度依赖 Leaflet 生态的项目里,这个误判的代价是 8 个架构级问题,继续修复最多只能解决 12/20 个问题。
4.2 重写方案:保留 Leaflet 引擎
第三次尝试的策略很朴素:整体迁入官方开源前端的 Leaflet 代码,直连 API 先跑通,跑通后再把数据迁移到自建 COS。保留引擎,8 个架构问题全部消失。
关键配置如下:
// src/map/map-init.js import L from 'leaflet' import 'leaflet/dist/leaflet.css' // 这一行不能省,后面会讲为什么 export function createMap(containerId, center) { const map = L.map(containerId, { crs: L.CRS.Simple, minZoom: 10, maxZoom: 13, zoomControl: false, attributionControl: false, center: [center.y, center.x], zoom: 11 }) L.tileLayer('/tiles/twt642/{z}/{x}/{y}.png', { tileSize: 256, minZoom: 10, maxZoom: 13, noWrap: true, bounds: [[-4096, -4096], [4096, 4096]] }).addTo(map) return map }4.3 坐标对齐:683 像素偏移的教训
用户反馈"黄点点和地图没有正确对应上"。我让 GLM-5.1 找原因,它去翻了源码,找到center: [3568, 6286]这个 fallback 值,套上去——用户反馈"比以前好一点还是差一些"。
后来用 Playwright 打开目标站点监听所有 JSON 响应,一分钟后就发现真实配置文件不叫web-map.json而是webapp.json,里面的center是[3568, 6969]——Y 值差了 683。
// 坐标校验脚本 async function verifyCenter() { const response = await fetch('/api/config/webapp.json') const config = await response.json() console.log('真实 center:', config.center) // 对比你代码里的 center,差值超过 1 像素就要查 const expected = [3568, 6969] const diff = Math.abs(config.center[1] - expected[1]) if (diff > 1) { console.warn(`Y 轴偏移 ${diff} 像素,检查是否用了 fallback 默认值`) } }这个坑的本质是:AI 读到 fallback 默认值就用了,没想到去抓真实运行时配置。你可以在项目里加一个启动时的配置校验,把这类问题挡在开发阶段。
4.4 瓦片坐标与负值处理
瓦片 ID 不是文档里的twt40而是twt642,瓦片源不是tiles.yuanshen.site(那是个文件浏览器)而是assets.yuanshen.site。更麻烦的是瓦片坐标有负值,旧脚本只覆盖正坐标。
# 瓦片下载脚本片段 def tile_range(z): n = 2 ** z # 自定义坐标系下范围是 [-n/2, n/2] half = n // 2 return range(-half, half) for z in range(10, 14): for x in tile_range(z): for y in tile_range(z): url = f"https://assets.yuanshen.site/tiles/twt642/{z}/{x}/{y}.png" # 下载逻辑批量下载脚本容易被 shell 600s 超时杀掉,用nohup后台跑:
nohup python3 download_tiles.py > tiles.log 2>&1 &5. 验证请求与成功结果
5.1 一行 CSS 让模型卡了 15 分钟
Leaflet 集成进 Vue3 后,打开页面看到的是一个诡异的大椭圆形暗角圆环:白色背景,边缘是 radial-gradient 过渡,没有任何瓦片、标记或 UI 元素可见。后端 API 全部返回 200,Leaflet 实例成功创建,DOM 里.leaflet-map-pane也存在,但就是没有瓦片渲染在视口里。
排查花了 15 分钟,试了 CRS 投影配置、mapPane transform、center 覆盖、merge 顺序,全部没用。最后用 Playwright 检查 DOM,发现所有.leaflet-pane元素的position都是static——而 Leaflet 默认应该是absolute。
根因:MapPage.vue缺少一行import 'leaflet/dist/leaflet.css'。
Leaflet 的 CSS 里定义了.leaflet-pane { position: absolute; }和.leaflet-map-pane { transform-origin: 0 0; }。没有这个 CSS,所有 pane 保持默认的position: static,translate3dtransform 无法正确偏移内容,瓦片全部渲染在视口外。
加上这一行,150/150 瓦片瞬间全部可见。
提示:TS 编译器对这种不导出值的 side-effect import 不报错也不警告,所以 AI 翻译时很容易把它当作"无用 import"忽略。你可以在 ESLint 里加一条规则,禁止删除 CSS side-effect import。
5.2 渲染验证动作
跑通后,用下面这套动作验证渲染是否正确:
// 渲染验证脚本,在浏览器控制台执行 function verifyRender() { const map = document.querySelector('.leaflet-container') const panes = document.querySelectorAll('.leaflet-pane') const tiles = document.querySelectorAll('.leaflet-tile-loaded') console.log('地图容器存在:', !!map) console.log('pane 数量:', panes.length) console.log('已加载瓦片:', tiles.length) // 检查 pane 定位 panes.forEach((pane, i) => { const pos = getComputedStyle(pane).position if (pos !== 'absolute') { console.error(`pane ${i} position 异常: ${pos}`) } }) // 检查瓦片是否在视口内 const viewport = map.getBoundingClientRect() let inViewport = 0 tiles.forEach(tile => { const rect = tile.getBoundingClientRect() if (rect.right > viewport.left && rect.left < viewport.right) { inViewport++ } }) console.log('视口内瓦片:', inViewport) } verifyRender()预期结果:pane 全部absolute,视口内瓦片数量大于 0,地图可拖动缩放。
5.3 标记点渲染验证
// 标记点验证 function verifyMarkers() { const markers = document.querySelectorAll('.leaflet-marker-icon') console.log('标记点数量:', markers.length) // 检查第一个标记的坐标 if (markers.length > 0) { const first = markers[0] const transform = first.style.transform console.log('第一个标记 transform:', transform) // 解析 translate3d(x, y, 0) const match = transform.match(/translate3d\(([^p]+)px,\s*([^p]+)px/) if (match) { console.log('标记坐标:', { x: parseFloat(match[1]), y: parseFloat(match[2]) }) } } } verifyMarkers()如果标记点数量为 0,检查selectedItemIds.size === 0时是否返回了全部 markers(这是个常见 bug,10 万个点位一次性渲染会卡顿)。
6. 本篇常见错排查
6.1 Python 数据管道的 5 个 CRITICAL 数据损毁
这是整个项目最深的教训。Python 脚本能跑、JSON 能生成、上传 COS 能访问、前端能加载、地图能渲染——但所有功能都是错的。不是 404,不是类型错误,是数据语义错误。
| Bug 位置 | AI 写的代码 | 后果 |
|---|---|---|
map_sync.py第 261 行 | item_ids = [it["itemId"] for it in item_list if "itemId" in it] | 只取 itemId,丢掉 count 字段,进度条永远 0% |
split_data.py第 86 行 | if name.lstrip("-").isdigit(): icon_ids.add(int(name)) | 只接受纯数字文件名,中文/哈希图标全过滤 |
map_sync.py第 266 行 | "title": m.get("markerTitle", "") | 存为 title,前端读 markerTitle,弹窗标题永远显示"点位信息" |
map_sync.pytransform_items | 只存 id/name/icon/typeIds/areaId/markerIds | 没存 specialFlag 和 sortIndex,传送点开关失效 |
map_sync.py第 220 行 | "children": [c["id"] for c in a.get("children", [])] | 子地区存为 int 列表,后续当 dict 遍历,TypeError 崩溃 |
根因都是同一个:GLM-5.1 写转换函数时,没有先读前端代码确认依赖哪些字段,而是基于 API 返回结构"自行精简"。
修复方式:从原始 API 数据重跑split_data.py,覆盖生成新数据。但更重要的是加一道校验:
# 数据完整性校验 REQUIRED_MARKER_FIELDS = ['id', 'itemId', 'count', 'markerTitle', 'x', 'y'] REQUIRED_ITEM_FIELDS = ['id', 'name', 'icon', 'typeIds', 'areaId', 'specialFlag', 'sortIndex'] def validate_markers(markers): for m in markers: missing = [f for f in REQUIRED_MARKER_FIELDS if f not in m] if missing: raise ValueError(f"标记 {m.get('id')} 缺少字段: {missing}")6.2 前端常见错
ItemSelector 宽 6416px:rem 缩放不生效,需动态设htmlfont-size。
选地区后列表为空:只检查selectedChildAreaId,顶级地区时为 null。要同时检查selectedAreaId。
COS 中文图标 404:double-encode 问题,中文文件名需要两次 encodeURIComponent。
图标存在也 403:翻译时照抄了源码的referrerpolicy="no-referrer",去掉即可。
地区面板死循环不显示:areaFirstChildcomputed 依赖未初始化的areaSelectedTop,加空值判断。
6.3 模型对话验证配置
如果你不确定某段 Leaflet 配置是否正确,可以把代码贴到模型对话页面快速验证:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat比如问:"Leaflet CRS.Simple 下,center 设为 [3568, 6969],zoom 11,瓦片范围 [-4096, 4096],这个配置能正确渲染吗?"模型会帮你检查坐标系和边界。
6.4 接入文档与 API Keys
遇到 API 参数问题、Key 权限问题,先查接入文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=docKey 管理在 API Keys 页面:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys7. 语义一致 CTA:按场景分流
7.1 排障与接入场景
如果你正在被"代码能跑但视觉不对"、"数据加载了但功能异常"这类问题折磨,建议先把 Key 和接入通道理顺,避免在排查时分心:
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
7.2 验证模型能力场景
想快速验证 GLM-5.1 对某段 Leaflet 配置、某个坐标变换、某个数据字段的理解,用模型对话页面:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat7.3 长期编码与 Agent 场景
如果你要长期做地图复刻、数据管道、多轮代码翻译,建议走 Coding Plan,额度更稳定,适合 Agent 类长程任务:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan7.4 Claude Code 接入
如果你用 Claude Code 做开发,Anthropic 兼容通道在这里:
https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=claude-code-anthropic最后说一个我踩过的坑:Python 数据管道写完一定要加字段校验,不要相信"脚本能跑就是对的"。前端表现异常时,先查数据层,再查渲染层。地图类项目里,坐标偏移 1 像素和字段丢 1 个,表现可能都是"看起来不太对",但排查方向完全不同。