threesmap
v0.4.0
Published
纯 three.js 实现的 Vue 3 3D 地图组件,不依赖 TresJS / R3F,内置示例数据、贴图与图层开关,零配置即可运行。
Downloads
817
Maintainers
Readme
threesmap
纯 three.js 实现的 Vue 3 3D 地图组件,不依赖 TresJS / R3F 等任何 Vue 3D 框架。 内置示例数据与贴图,零配置即可运行;支持省 → 市下钻、区县信息卡与企业布点聚合。
- 纯 three.js,不侵入你的技术栈
- 内置安徽省示例 GeoJSON + 8 张贴图,装完即用(无配套法线贴图)
- 下钻:点击区域进入该市的区县视图,面包屑返回,镜头平滑过渡
- 企业布点:DOM 标点 + 悬浮卡片 + 点击聚焦 + 按屏幕像素自动聚合
- 图层开关:云层 / 边界流光 / 底部光环 / 数据柱 / 热力图 / 企业打点 / 是否允许下钻
- 完整 TypeScript 类型;卸载时自动释放贴图、几何体、材质、渲染器与 DOM
安装
npm i threesmap
npm i three vue # 这两个是 peer 依赖,需要你自己装| 依赖 | 版本 | 说明 |
| --- | --- | --- |
| vue | ^3.3 | peer dependency |
| three | >= 0.160 < 0.190 | peer dependency。已在 0.186 验证,0.160 ~ 0.18x 均可用;打包时必须 external,否则会出现两份 three 实例 |
| gsap、d3-geo | —— | 随包自动安装 |
| heatmap.js | v2.0.5 | 已内置(vendored,12 KB 随包打包):热力图开箱即用,无需额外安装。见根目录 LICENSE 中对原作者 Patrick Wied 的署名 |
three 的版本区间为什么不用
^0.160:semver 对0.x的 caret 只允许补丁位变化,^0.160.0实际等于>=0.160.0 <0.161.0,会把 0.161 ~ 0.18x 全挡在外面。
本包只提供 ESM 产物,需通过打包工具使用:Vite(含 Vitest / Nuxt 等)、 webpack 5+、rspack、esbuild 均可;内置贴图与示例数据是按需加载的独立分包, 因此不支持 webpack 4 及更早版本,也不支持
require()直接引入。
不想走 npm? 也可以把
src/ThreeMap/整个目录拷进你的工程直接用 (import ThreeMap from "@/components/ThreeMap/ThreeMap.vue")。该目录是自包含的: 只需three / gsap / d3-geo,零额外构建配置,热力图引擎已 vendored 在内。 细节与注意事项见 src/ThreeMap/README.md。
产物结构
内置贴图与示例数据不进主产物,用到才下载:
| 内容 | 体积 | 加载时机 |
| --- | --- | --- |
| 组件代码 threesmap.mjs | 约 119 KB(gzip 38 KB) | 随你的应用一起打包(含内置热力图引擎 heatmap.js 12 KB) |
| 内置贴图(8 张) | 约 1.4 MB | 挂载后按需下载,每张一个分包;只加载你没覆盖的 key |
| 内置 GeoJSON(省 / 区县) | 约 0.13 MB / 0.82 MB | 用到才下载(16 市 / 104 区县) |
- 不传任何 props 时,主产物只有百 KB 量级的代码,贴图与大地图数据都是网络按需加载;
- 想连这 1.4 MB 内置资产都不要?用精简入口
threesmap/core(约 104 KB,零内置数据与贴图):
// 换省份 / 换国家的业务侧推荐:不会为用不到的内置安徽资产买单
import ThreeMap from "threesmap/core";
import "threesmap/style.css"; // 样式与主入口共用同一份
// 必须自己传 mapData(贴图可选),未传时组件给出开发期告警不想经 npm、想把源码整目录拷进项目?差异说明见
src/ThreeMap/README.md——除导入路径与样式引入方式外,用法与本手册一致。
30 秒跑通
<template>
<div class="map-page">
<ThreeMap />
</div>
</template>
<script setup lang="ts">
import ThreeMap from "threesmap";
import "threesmap/style.css";
</script>
<style scoped>
/* 父容器必须有明确高度:组件内部是 100% × 100% */
.map-page {
width: 100%;
height: 100vh;
}
</style>就这一步。不传任何 props 时用的是内置的安徽省数据与贴图。
功能导航
| 想要的效果 | 看这一节 |
| --- | --- |
| 数据怎么配才不会"不显示"(key 规则、量级约定) | 0. 数据契约 |
| 换成自己的地图 / 业务数据 / 贴图 | 1. 基础地图 |
| 图层开关、工具栏 | 2. 图层开关 |
| 点击区域下钻到下一级 | 3. 下钻 |
| 下级区域 hover 显示业务数据 | 4. 下级区域信息 |
| 在地图上打企业点、聚合、点击 | 5. 企业布点 |
| 迁徙飞线、流光配色、自定义提示 | 6. 迁徙飞线 |
| 查 props / 事件 / 实例方法 / 动态更新 | API 速查 |
| 控制台那条 [ThreeMap] 告警是什么意思 | 控制台告警速查 |
| 性能开销、畸形数据容错、dispose() 契约 | 注意事项 |
0. 数据契约
这一节是最容易踩坑的地方:配错了组件不会报错,只是"那块东西不显示"。
好在开发期组件会直接把这些情况打到控制台(前缀 [ThreeMap]),照着提示改即可。
key 规则
三类业务数据的 key 统一为 adcode:
| 数据 | key | 说明 |
| --- | --- | --- |
| mapHoverData(顶层悬浮卡) | adcode,如 "340100" | 顶层要素(市级)的 adcode |
| subMapHoverData(下级悬浮卡) | adcode,如 "340102" | 下级要素(区县级)的 adcode |
| enterpriseData / subEnterpriseData | adcode | 同上 |
区域名(
properties.name)只用于展示(地图标签、卡片标题),不做 key—— 名称会跨市重名(比如多个"城关区"),adcode 才是稳定标识。 误把名称当 key 时控制台会给出map-hover-data 的 key 应该是 adcode(如 "340100"),但检测到名称 key:…的提示, 并列出当前层可用的 adcode 示例,直接照抄即可。
量级约定
| 字段 | 要求 | 配错的后果 |
| --- | --- | --- |
| mapHoverData[adcode].population | 单位为万(2000 = 2000 万)。柱高 = population / 50 | 数值太小(比如 20)时柱子比地图自身厚度还矮,看起来像"没有数据柱" |
| 热力图点的 value | 1000 ~ 2000(强度映射区间在热力图层内固定) | 低于 1000 会被压成最弱强度,几乎看不见 |
顶面贴图
贴图必须严格按顶层 GeoJSON 的经纬度包围盒裁剪(含所有岛屿、无留白、纵横比与投影后一致)——
UV 是按包围盒归一化映射的,范围不一致就会"只贴对一部分"。
想先确认几何对不对,把 textures.map 传 null 用纯色顶面看一眼最快。
什么时候传数据
mapData / subMapData / 悬浮卡 / 企业数据要求初始化时传入;
飞线与热力图支持运行时替换(见 API 速查 的"动态更新"),
其余数据如需运行时更换,用实例方法 setSubMapData,或重建组件。
1. 基础地图
传了就用你的,没传的继续用内置默认值。
<template>
<ThreeMap
:map-data="myMapData"
:map-hover-data="myHoverData"
:textures="{ map: myMapTexture }"
:depth="6"
background="#fff5e8"
/>
</template>
<script setup lang="ts">
import ThreeMap from "threesmap";
import type { CityGeoJSON, CityInfo } from "threesmap";
import "threesmap/style.css";
import myMapJson from "@/data/myMap.json";
import myMapTexture from "@/assets/my_map.png";
const myMapData = myMapJson as unknown as CityGeoJSON;
// key 是**要素的 adcode**(不是区域名),内容决定悬浮卡文字与数据柱高度
const myHoverData: Record<string, CityInfo> = {
340100: { population: 963, gdp: "1.27万亿", area: "11445平方公里" },
};
</script>GeoJSON 数据要求
- 必须是
FeatureCollection;几何支持MultiPolygon与Polygon,GeometryCollection会被自动展平(内部统一成MultiPolygon) properties至少要提供:
| 字段 | 用途 |
| --- | --- |
| name | 区域名,用于地图标签与卡片标题(不做业务数据的 key,key 一律用 adcode) |
| centroid 或 center | [经度, 纬度],用于确定标签、数据柱的位置 |
| adcode | 行政区划代码。下钻、区县数据、企业数据都靠它关联,强烈建议提供 |
| parent.adcode | 上级 adcode。下钻时默认据此找下级,没有则用 resolveChildren 自定义 |
贴图
textures 可以只覆盖部分 key,其余继续用内置图——被覆盖的 key 不会去下载对应的内置图:
import myMapTexture from "@/assets/my_map.png";
// 只换地图主贴图:其余 7 张内置图照常按需下载
<ThreeMap :textures="{ map: myMapTexture }" />全部 9 个 key:map、normalMap、barQuan、bottomGaoguang、bottomGrid、bottomGridBlack、bottomRotation1、bottomRotation2、cloud。
内置示例只提供其中 8 个(normalMap 为空:示例没有配套法线贴图);
需要程序化拿到内置图片地址时用 loadDefaultTextures()(异步,可只取子集):
import { loadDefaultTextures } from "threesmap";
const textures = await loadDefaultTextures(["map", "cloud"]); // 只想复用这两张
// <ThreeMap :textures="{ ...textures, bottomGrid: null }" /> null = 显式禁用某张内置贴图每个 key 的 wrap / colorSpace / repeat 属于材质固有配置,已内置,外部只需给图片地址。地址支持相对路径、import 出来的资源 URL、远程 http(s)、data:、blob:。
2. 图层开关
options 支持响应式变更(内部 deep watch 同步显隐),配合内置的 MapToolbar 可以直接做出开关工具栏:
<template>
<div class="map-page">
<ThreeMap :options="options" />
<MapToolbar v-model="options" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ThreeMap, MapToolbar } from "threesmap";
import type { ThreeMapOptions } from "threesmap";
import "threesmap/style.css";
const options = ref<ThreeMapOptions>({
cloud: true,
boundary: true,
rotation: true,
bar: false,
heat: false,
drillDown: true,
});
</script>| 字段 | 默认 | 说明 |
| --- | --- | --- |
| cloud | true | 云层 |
| boundary | true | 行政区划边界流光 |
| rotation | true | 底部旋转光环与网格 |
| bar | false | 数据柱(高度取 mapHoverData 的 population) |
| heat | false | 热力图 |
| enterprise | true | 企业打点。两层共用这一个开关:顶层读 enterpriseData,下钻后读 subEnterpriseData |
| tooltip | true | 悬浮信息卡(区域卡 / 企业卡)。关闭只是不弹卡片,高亮、标点、点击聚焦、簇列表都照常 |
| flyLine | true | 迁徙飞线(仅顶层)。不传 flyLineData 时自然为空 |
| drillDown | true | 是否允许下钻。关掉后点击区域仍抛 city-click,但不切换层级,便于业务自己接管 |
所有图层都在初始化时一次性构建,开关只切
visible,所以切换是即时的、没有任何加载。 代价是:想让热力图后续能开关,必须在初始化时就传入heatmapData;传null表示完全不创建。
3. 下钻
点击某个市即进入该市的下钻态:场景里只保留该市的区县,其余市级要素被移除。
下钻态的点击不再切层级:点区县只切换本地高亮(再点同一区县取消),并抛出 @sub-city-click(见下方事件表);点地图外空白处会把视角飞回当前层的默认位置。
需要什么数据
| Prop | 说明 |
| --- | --- |
| mapData | 顶层(省)GeoJSON |
| subMapData | 全量下级(区县)GeoJSON。下钻时按 parent.adcode 过滤出当前市的子集 |
不传任何数据时,组件用内置的安徽示例数据直接渲染(零配置可用)。一旦传了自己的 mapData,内置示例业务数据(下级区县 / 城市数据 / 热力图)全部不再兜底——省份对不上,需要哪份就显式传哪份;换省份 = 成对替换 mapData 与 subMapData(或替换 assets/ 里的两个 JSON),组件内部没有任何省份/城市字面量。
配置
<ThreeMap
:map-data="myMapData"
:sub-map-data="mySubMapData"
:drill-down-config="drillConfig"
@city-click="onCityClick"
@sub-city-click="onSubCityClick"
@level-change="onLevelChange"
/>import type { DrillDownConfig } from "threesmap";
const drillConfig: DrillDownConfig = {
topLevelName: "安徽省", // 面包屑里的顶层名称,默认「全省」
cameraDuration: 900, // 镜头过渡时长 ms,默认 800
};| 字段 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| topLevelName | string | "全省" | 面包屑顶层名。GeoJSON 没有"省"这一级要素,只能由配置提供 |
| resolveChildren | (city, areas) => areas | 比较 parent.adcode | 自定义"谁是谁的下级" |
| cameraDuration | number | 800 | 镜头过渡时长(ms) |
| cluster | boolean | true | 企业是否聚合,见第 5 节 |
| clusterRadius | number | 44 | 聚合阈值(屏幕像素) |
| focusDistance | number | 按层级自适应 | 点击企业后镜头拉近到的距离 |
| districtFields | FieldLabels | —— | 区县卡片字段,见第 4 节 |
| enterpriseFields | FieldLabels | —— | 企业卡片字段,见第 5 节 |
事件
| 事件 | 参数 | 触发时机 |
| --- | --- | --- |
| @city-click | { adcode, name } | 点击顶层区域。下钻失败(无下级数据)时同样会抛 |
| @sub-city-click | { adcode, name, city, selected } | 下钻态点击下级区域。不切层级、只切高亮,抛在切换之后,所以 selected 就是点击后的最终态(再点同一区县为 false) |
| @level-change | { level, city } | 层级变化。level 为 "province" / "city" |
@level-change 常用于让业务自己的覆盖层跟着层级显隐——比如图层工具栏只该在省级显示:
<ThreeMap :options="options" @level-change="onLevelChange" />
<MapToolbar v-if="levelState.level === 'province'" v-model="options" />import { ref } from "vue";
import type { LevelState } from "threesmap";
const levelState = ref<LevelState>({ level: "province", city: null });
const onLevelChange = (state: LevelState) => (levelState.value = state);用外部 UI 驱动层级
组件实例暴露了一组方法,配合 ref 即可用任意自己的按钮/菜单控制层级:
<template>
<ThreeMap ref="mapRef" :sub-map-data="mySubMapData" />
<button @click="mapRef?.drillDown(340100)">进入合肥市</button>
<button @click="mapRef?.drillUp()">返回</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ThreeMap } from "threesmap";
const mapRef = ref<InstanceType<typeof ThreeMap> | null>(null);
</script>| 方法 | 签名 | 说明 |
| --- | --- | --- |
| drillDown | (adcode: number) => boolean | 进入指定区域。没有下级数据时返回 false 且不切换层级 |
| drillUp | () => void | 返回上一层 |
| getLevel | () => LevelState | 读取当前层级 |
| setSubMapData | (data: CityGeoJSON) => void | 回填下级 GeoJSON(数据大时先挂载、就绪后回填) |
| setFlyLineData | (data: FlyLineData \| null) => void | 运行时替换迁徙飞线数据 |
| setHeatmapData | (data: HeatmapData \| null) => void | 运行时替换热力数据 |
动态更新
哪些数据能"改了就生效"、哪些必须重新挂载组件,一览:
| 数据 | 更新方式 |
| --- | --- |
| flyLineData | 改 prop(组件已 watch)或调 setFlyLineData() |
| flyLineColors | 改 prop(会重建飞线层,用新配色) |
| heatmapData | 改 prop(组件已 watch)或调 setHeatmapData() |
| subMapData | 调 setSubMapData();也可不传,等异步加载完再回填 |
| options(图层开关) | 改 prop,即时生效(只切显隐,不重建对象) |
| mapData / 悬浮卡 / 企业数据 | 需重新挂载组件(参与几何与图层构建) |
4. 下级区域信息
下钻后鼠标悬浮区县会弹出信息卡,内容由 subMapHoverData 驱动,key 是 adcode:
<ThreeMap
:sub-map-data="mySubMapData"
:sub-map-hover-data="subMapHoverData"
:drill-down-config="{ districtFields: { population: '人口(万)', enterprises: '企业数' } }"
/>const subMapHoverData = {
340102: { population: 146, enterprises: 46 }, // 瑶海区
340103: { population: 102, enterprises: 38 }, // 庐阳区
};
// districtFields 的对象插入顺序 = 卡片里的渲染顺序
// 这里只显示 population 与 enterprises,其余字段忽略subMapHoverData的值是自由字段对象,想展示哪些由districtFields决定- 某区域没有配数据时,信息卡只显示名称(不会报错)
5. 企业布点
企业数据以 adcode 为 key,只有在对应要素可见时才渲染。两层各有一套数据:
| Prop | 生效层级 | key |
| --- | --- | --- |
| enterpriseData | 顶层(如省视图下的各市) | 顶层要素 adcode |
| subEnterpriseData | 下钻后(如市视图下的各区县) | 下级要素 adcode |
两者共用一个开关 options.enterprise(默认开启),关闭时标点、标签、悬浮卡与簇列表一并隐藏。
<ThreeMap
:sub-map-data="mySubMapData"
:enterprise-data="enterpriseData"
:drill-down-config="{
enterpriseFields: { industry: '行业', scale: '规模', employees: '员工数' },
cluster: true,
clusterRadius: 46,
}"
@enterprise-click="onEnterpriseClick"
/>import type { Enterprise } from "threesmap";
const enterpriseData: Record<string, Enterprise[]> = {
340102: [
{ id: 1, name: "某电子信息公司", lng: 104.115, lat: 30.601,
industry: "电子信息", scale: "大型", employees: 860 },
],
};字段要求
| 字段 | 必需 | 说明 |
| --- | --- | --- |
| id | ✅ | 唯一标识 |
| name | ✅ | 标点常显的名称 |
| lng / lat | ✅ | 经纬度 |
| 其它 | —— | 任意扩展字段,用 enterpriseFields 决定卡片展示哪些 |
- 缺必需字段或经纬度非法的记录会被跳过并告警,不会中断整层渲染
- 落在当前市范围外的企业会被裁掉,避免脏点飘在屏幕外
- key 必须是 adcode,不是区县名(区县名会跨市重名)。写错时开发期会输出警告
交互行为
| 操作 | 行为 |
| --- | --- |
| 悬浮标点 | 显示信息卡(字段由 enterpriseFields 决定顺序) |
| 点击标点 | 抛出 enterprise-click,同时镜头只推近、不改变观察角度 |
| 悬浮聚合簇 | 显示该簇包含的企业数量 |
| 点击聚合簇 | 镜头拉近以拆开该簇;已到最近距离仍拆不开时,弹出企业列表浮层 |
聚合
- 判据是屏幕像素距离(默认 44px),所以镜头拉近后标点会自然散开,不需要"展开"动画
- 相机下探到该层最近距离后,成员数 ≤ 3 的小簇会直接展开为独立标点——避免出现"只剩两三家却怎么也拆不开、看不到是谁"的死角
cluster: false可整体关闭聚合,所有企业都渲染为独立标点
点击事件
import type { Enterprise, EnterpriseClickContext } from "threesmap";
const onEnterpriseClick = (enterprise: Enterprise, ctx: EnterpriseClickContext) => {
console.log(enterprise.name, ctx.district, ctx.city);
// ctx = { district: "瑶海区", city: { adcode: 340100, name: "合肥市" } }
};6. 迁徙飞线
只传一个数组就能出效果,端点是 { name, lng, lat }:
<ThreeMap :fly-line-data="flyLineData" />const flyLineData = [
{ from: { name: "芜湖市", lng: 118.38, lat: 31.33 },
to: { name: "合肥市", lng: 117.28, lat: 31.86 }, value: 1280 },
// ...再放几条
];效果构成:抛物线拱起的管状弧线(拱高随起终点距离变化)+ 沿线循环前进的流动亮带(各线相位错开)+ 起点呼吸圆环 / 终点光点。鼠标悬浮到线上会提亮该线、压低其余线,并弹出提示。
配色可配(5 个色位,只传要改的):
<ThreeMap
:fly-line-data="flyLineData"
:fly-line-colors="{ track: '#7dd3fc', flow: '#0ea5e9', core: '#38bdf8' }"
/>取色规律:浅色底图上"更亮"表达不出强度,只能用"更饱和",所以默认是浅青轨道 + 饱和青流光。 如果底图换成深色,整组改霓虹色(如
track: '#0e7490', flow: '#22d3ee')会更好看。
提示的两种扩展方式:
// 1) 传函数,完全接管内容
const flyLineTip = (item) =>
`<b>${item.from.name} → ${item.to.name}</b><br/>运量 ${item.value} 人次`;/* 2) 不改组件,直接覆盖内置提示元素的样式 */
.tm-fly-tip { border-radius: 4px; }内置提示元素带 tm-fly-tip class 与 data-from / data-to / data-value 属性,方便外部取用。
飞线支持运行时替换:改
flyLineDataprop 或调setFlyLineData(),组件会重建该图层; 改flyLineColors也会用新配色重建。飞线只在顶层出现,下钻态自动隐藏。
API 速查
Props
| Prop | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| mapData | CityGeoJSON | 内置安徽省数据 | 顶层 GeoJSON |
| subMapData | CityGeoJSON | 内置(动态加载) | 全量区县 GeoJSON,下钻用 |
| mapHoverData | Record<string, CityInfo> | 内置(仅未传 mapData 时) | 顶层悬浮卡数据,key 为顶层要素 adcode;同时决定数据柱高度 |
| subMapHoverData | SubMapHoverData | —— | 下级悬浮卡数据(下钻后),key 为 adcode |
| enterpriseData | EnterpriseData | —— | 顶层企业布点,key 为顶层要素 adcode |
| subEnterpriseData | EnterpriseData | —— | 下钻后企业布点,key 为下级要素 adcode |
| flyLineData | FlyLineData | —— | 迁徙飞线:[{ from, to, value? }],端点是 { name, lng, lat } |
| flyLineTip | FlyLineTipRenderer | —— | 自定义飞线悬浮提示:(item, index) => string \| HTMLElement \| null |
| flyLineColors | FlyLineColors | 内置青蓝 | 飞线配色:{ track, flow, core, ring, dot },只传要改的色位 |
| drillDownConfig | DrillDownConfig | —— | 下钻行为统一配置口 |
| heatmapData | HeatmapData \| null | 内置(仅未传 mapData 时) | 传 null 表示不创建热力图 |
| textures | ThreeMapTextures | 内置 8 张(normalMap 为空) | 可只覆盖部分 key |
| options | ThreeMapOptions | 见第 2 节 | 图层与交互开关 |
| depth | number | 6 | 地图挤出厚度(各层级按跨度等比缩放) |
| cameraPosition | [number, number, number] | [-50, 125, 250] | 相机初始位置(入场动画的起点) |
| cameraView | CameraView | 内置默认 | 相机视角:决定镜头从哪个方向看地图,所有层级生效 |
| background | string | #fff5e8 | 背景色 |
cameraView 用方位角 / 仰角描述镜头方向,比 xyz 直观:
| 字段 | 默认 | 说明 |
| --- | --- | --- |
| azimuth | 20.6 | 方位角(度):0 = 从正前方看,90 = 从右侧看 |
| elevation | 36.2 | 仰角(度):0 = 平视,90 = 正上方俯视 |
| distance | 1 | 距离倍数:相对默认视角距离(0.8 = 拉近 20%)。与 scale 二选一 |
| scale | 1 | 放大倍数:2 = 放大两倍(≡ distance: 0.5),比 distance 直观;两者同时传以 scale 为准 |
<!-- 下面这组就是内置默认视角(不传 cameraView 时即为此);要调就改这几个数 -->
<ThreeMap :camera-view="{ azimuth: 20.6, elevation: 36.2, scale: 1 }" />对所有层级生效(入场画面 / 下钻与返回 / 点空白复位 / 点聚合簇推近),所以切换层级时视角保持一致;
距离部分仍按各层范围自适应,scale / distance 只是统一的倍数。
不传则完全沿用内置默认视角,行为不变。
Events
| 事件 | 参数 |
| --- | --- |
| @play-complete | 无(相机推进 + 图层淡入 + 整体缩放结束) |
| @city-click | { adcode, name } |
| @sub-city-click | { adcode, name, city, selected } |
| @enterprise-click | (enterprise, { district, city }) |
| @level-change | { level, city } |
实例方法(defineExpose)
| 方法 | 说明 |
| --- | --- |
| drillDown(adcode) | 进入指定市,无下级数据时返回 false |
| drillUp() | 返回省级 |
| getLevel() | 读取当前 LevelState |
| setSubMapData(data) | 回填下级 GeoJSON(数据大时先挂载、就绪后回填) |
| setFlyLineData(data \| null) | 运行时替换迁徙飞线数据 |
| setHeatmapData(data \| null) | 运行时替换热力数据 |
完整签名与用法见 用外部 UI 驱动层级。 组件卸载时会自动释放全部资源(含 WebGL 上下文);
dispose()之后这些方法都会直接返回。
包的其它导出
除组件外,threesmap 还导出了内置数据与底层工具,便于按需复用:
import {
// 内置数据(可用于兜底或参考格式)
defaultMapHoverData, defaultHeatmapData,
defaultTextureKeys, // 内置贴图的 key 清单(8 个,不含 normalMap)
defaultOptions,
loadDefaultTextures, // 按需加载内置贴图地址,返回 Promise(每个 key 一个懒加载分包)
loadDefaultMapData, // 异步加载内置省级 GeoJSON,返回 Promise(独立分包)
loadDefaultSubMapData, // 异步加载内置区县数据,返回 Promise(独立分包)
// 底层工具
loadTexture, textureLoaders, createThreeMap,
buildLevelData, computeLevelMetrics, indexByParent,
projectFeatureBounds, defaultResolveChildren,
} from "threesmap";完整清单见
src/ThreeMap/index.ts。createThreeMap是纯 three.js 的场景工厂,脱离 Vue 也能用。
注意事项
- 父容器必须有明确高度,否则画布高度为 0(组件内部是
100% × 100%)。 - 组件在挂载后异步批量加载贴图,全部就绪才创建场景。单张贴图失败(404 / 无 CORS / 超时 15s)只会丢掉那一张,不影响其余贴图与地图渲染。
- 下钻态会强制打开边界流光、关闭云层 / 数据柱 / 热力图(那套光效是按省级尺度搭建的,拉近后只剩几道巨大弧线,很突兀);返回时按你的开关原样还原。
- 体积:运行时入口约 119 KB(已含内置热力图引擎);8 张内置贴图(约 1.4 MB)
与两级 GeoJSON 都是按需下载——自带地图与贴图的项目运行时不请求这些示例资源
(见上文「产物结构」)。想连这些资产都不装,用精简入口
threesmap/core(约 104 KB)。 - 内置的 8 张贴图是示例资源,商用请替换成你自己的图片。
内置贴图以
data:URL 形式加载,若站点配置了 CSP,需保证img-src允许data:; 自己传 URL 的贴图不受此影响。 - 组件卸载时会自动释放贴图、几何体、材质、渲染器、WebGL 上下文、控制器监听与注入的 DOM。 GL 上下文不会被残留(浏览器对同时存活的上下文数量有上限,反复挂载 / 卸载也不会耗尽)。
- 开销只在需要时发生:CSS2D 标注层是按需刷新的——相机静止、且标注没有增删或显隐变化时,
一整帧不写一次标注 DOM;悬停判定与渲染帧对齐(
pointermove只记最新坐标,每帧最多判定一次)。 实测相机静止 5 秒的样式重算次数为 0(旧行为是每帧全量刷新,约 293 次)。 - 畸形数据不会白屏、也不会抛异常:坐标缺失 / 非法、
centroid与center双缺、coordinates为空、甚至mapData是空数组——都只会跳过对应内容并在控制台告警, 其余部分照常渲染。 - 直接用底层
createThreeMap(脱离 Vue)时,用完必须调dispose(): 它会释放 GL 上下文与控制器监听。dispose()之后该实例的所有方法都会直接返回, 不会再创建图层或重新挂载数据。
控制台告警速查
组件只在开发期告警、从不抛异常中断渲染(前缀统一为 [ThreeMap])。常见几条:
| 告警 | 含义 | 处理 |
| --- | --- | --- |
| … 的 key 应该是 adcode(如 "340100"),但检测到名称 key | 业务数据把区域名当 key 用了 | 改成对应要素的 properties.adcode |
| … 里没有任何 key 命中当前层的 adcode | key 与当前层级对不上 | 确认数据传对了层级(顶层 / 下级) |
| 该层级没有任何可投影的有效坐标…已放弃挂载该层 | GeoJSON 坐标全缺失或非法 | 检查 geometry.coordinates |
| 缺少下级数据,无法下钻 | 没传 subMapData | 传 subMapData,或用 setSubMapData 回填 |
| 贴图加载失败:<key> → <url> | 单张贴图 404 / 未开 CORS / 超时 15s | 该张贴图退化为无贴图,其余不受影响 |
| 内置贴图分包加载失败:<key> | 贴图数据分包缺失 / 网络失败 | 该张贴图退化为无贴图,其余不受影响 |
| 热力图图层创建失败,已跳过该图层 | 画布不可用或数据异常 | 只丢热力图,其他图层与开关不受影响 |
| 首屏挂载失败,场景为空 | 数据畸形让图层创建抛错 | 看告警后的错误详情定位数据 |
不想翻文档?让 AI 读它
仓库根目录的 CLAUDE.md 是面向 AI 助手的项目入口:包含技术栈与约束、
目录地图、常用命令、开发流程(OpenSpec 变更工作流)、编码与测试规范、发布流程,
以及一张"易踩坑清单"。把仓库丢给 AI 时,让它先读这个文件即可,省去逐篇翻 README。

