npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

threesmap

v0.4.0

Published

纯 three.js 实现的 Vue 3 3D 地图组件,不依赖 TresJS / R3F,内置示例数据、贴图与图层开关,零配置即可运行。

Downloads

817

Readme

threesmap

纯 three.js 实现的 Vue 3 3D 地图组件,不依赖 TresJS / R3F 等任何 Vue 3D 框架。 内置示例数据与贴图,零配置即可运行;支持省 → 市下钻、区县信息卡与企业布点聚合。

  • 纯 three.js,不侵入你的技术栈
  • 内置安徽省示例 GeoJSON + 8 张贴图,装完即用(无配套法线贴图)
  • 下钻:点击区域进入该市的区县视图,面包屑返回,镜头平滑过渡
  • 企业布点:DOM 标点 + 悬浮卡片 + 点击聚焦 + 按屏幕像素自动聚合
  • 图层开关:云层 / 边界流光 / 底部光环 / 数据柱 / 热力图 / 企业打点 / 是否允许下钻
  • 完整 TypeScript 类型;卸载时自动释放贴图、几何体、材质、渲染器与 DOM

pn3KdTf.md.png

安装

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 属性,方便外部取用。

飞线支持运行时替换:改 flyLineData prop 或调 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 也能用。


注意事项

  1. 父容器必须有明确高度,否则画布高度为 0(组件内部是 100% × 100%)。
  2. 组件在挂载后异步批量加载贴图,全部就绪才创建场景。单张贴图失败(404 / 无 CORS / 超时 15s)只会丢掉那一张,不影响其余贴图与地图渲染。
  3. 下钻态会强制打开边界流光、关闭云层 / 数据柱 / 热力图(那套光效是按省级尺度搭建的,拉近后只剩几道巨大弧线,很突兀);返回时按你的开关原样还原。
  4. 体积:运行时入口约 119 KB(已含内置热力图引擎);8 张内置贴图(约 1.4 MB) 与两级 GeoJSON 都是按需下载——自带地图与贴图的项目运行时不请求这些示例资源 (见上文「产物结构」)。想连这些资产都不装,用精简入口 threesmap/core(约 104 KB)。
  5. 内置的 8 张贴图是示例资源,商用请替换成你自己的图片。 内置贴图以 data: URL 形式加载,若站点配置了 CSP,需保证 img-src 允许 data:; 自己传 URL 的贴图不受此影响。
  6. 组件卸载时会自动释放贴图、几何体、材质、渲染器、WebGL 上下文、控制器监听与注入的 DOM。 GL 上下文不会被残留(浏览器对同时存活的上下文数量有上限,反复挂载 / 卸载也不会耗尽)。
  7. 开销只在需要时发生:CSS2D 标注层是按需刷新的——相机静止、且标注没有增删或显隐变化时, 一整帧不写一次标注 DOM;悬停判定与渲染帧对齐(pointermove 只记最新坐标,每帧最多判定一次)。 实测相机静止 5 秒的样式重算次数为 0(旧行为是每帧全量刷新,约 293 次)。
  8. 畸形数据不会白屏、也不会抛异常:坐标缺失 / 非法、centroid 与 center 双缺、 coordinates 为空、甚至 mapData 是空数组——都只会跳过对应内容并在控制台告警, 其余部分照常渲染。
  9. 直接用底层 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。