3D+动态涟漪点(支持少量)

地图组件:Shanxi3DRipplesMap.vue
html
<!-- 动态涟漪点 -->
<template>
<div class="map-container">
<div id="amap-container" class="map-wrapper"></div>
</div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import {
AMAP_KEY,
AMAP_SECRET,
AMAP_VERSION,
MAP_CENTER,
MAP_DEFAULT_ZOOM,
MAP_PITCH,
MAP_ROTATE,
} from "@/config/params.js";
export default {
name: "ShanXi3DRipplesMap",
data() {
return {
AMapIns: null, // 保存 AMap 实例,供 GeometryUtil 使用
map: null,
loca: null,
polygonLayer: null,
topLineLayer: null,
bottomLineLayer: null,
cityMarkers: [],
rippleMarkers: [], // ★ 新增:保存涟漪点实例,方便销毁
hoveredAdcode: null,
geoFeatures: [], // 保存市的几何数据,用于数学计算判断
};
},
mounted() {
this.initMap();
},
beforeDestroy() {
// ★ 新增:销毁涟漪点
if (this.rippleMarkers && this.rippleMarkers.length) {
this.map.remove(this.rippleMarkers);
}
if (this.cityMarkers && this.cityMarkers.length) {
this.map.remove(this.cityMarkers);
}
if (this.loca) {
this.loca.animate.stop();
this.loca.destroy();
}
if (this.map) {
this.map.destroy();
}
},
methods: {
initMap() {
window._AMapSecurityConfig = {
securityJsCode: AMAP_SECRET,
};
AMapLoader.load({
key: AMAP_KEY,
version: AMAP_VERSION,
Loca: { version: AMAP_VERSION },
plugins: ["AMap.TileLayer.Satellite", "AMap.TileLayer.RoadNet"],
})
.then((AMap) => {
this.AMapIns = AMap; // 保存 AMap 引用
this.map = new AMap.Map("amap-container", {
viewMode: "3D",
pitch: MAP_PITCH,
rotation: MAP_ROTATE,
zoom: MAP_DEFAULT_ZOOM,
center: MAP_CENTER,
showLabel: false,
showBuildings: false,
mapStyle: "amap://styles/a2a9b46da661bd97c1b6b028ae9e5ee7",
rotateEnable: false,
pitchEnable: false,
zoomEnable: false,
dragEnable: false,
backgroundColor: "rgba(0,0,0,0)",
});
this.loca = new Loca.Container({ map: this.map });
this.loadShanxiData();
})
.catch((e) => {
console.error("高德地图加载失败:", e);
});
},
async loadShanxiData() {
try {
const res = await fetch(
"https://geo.datav.aliyun.com/areas_v3/bound/140000_full.json"
);
const data = await res.json();
// 提取掩膜坐标
let maskCoords = [];
data.features.forEach((feature) => {
const geom = feature.geometry;
if (geom.type === "Polygon") {
maskCoords.push(geom.coordinates);
} else if (geom.type === "MultiPolygon") {
maskCoords.push(...geom.coordinates);
}
});
this.map.setMask(maskCoords);
// 将市级面数据转为线数据
const lineFeatures = [];
data.features.forEach((feature) => {
const geom = feature.geometry;
const coords = geom.coordinates;
let rings = [];
if (geom.type === "MultiPolygon") {
coords.forEach((polygon) => {
rings.push(...polygon);
});
} else if (geom.type === "Polygon") {
rings = coords;
}
rings.forEach((ring) => {
lineFeatures.push({
type: "Feature",
properties: feature.properties,
geometry: {
type: "LineString",
coordinates: ring,
},
});
});
});
const lineData = { type: "FeatureCollection", features: lineFeatures };
this.renderShanxi3D(data, lineData);
} catch (error) {
console.error("加载 GeoJSON 数据失败:", error);
}
},
renderShanxi3D(polygonGeoJson, lineGeoJson) {
// ★ 保存一份面数据给纯几何计算用
this.geoFeatures = polygonGeoJson.features;
const polygonSource = new Loca.GeoJSONSource({ data: polygonGeoJson });
const lineSource = new Loca.GeoJSONSource({ data: lineGeoJson });
if (this.polygonLayer) this.loca.remove(this.polygonLayer);
if (this.topLineLayer) this.loca.remove(this.topLineLayer);
if (this.bottomLineLayer) this.loca.remove(this.bottomLineLayer);
// ========== 2. 3D 拉伸区块 ==========
this.polygonLayer = new Loca.PolygonLayer({
zIndex: 1,
opacity: 1,
visible: true,
zooms: [2, 22],
evented: true,
});
this.polygonLayer.setSource(polygonSource);
const getStyle = () => ({
topColor: (index, feature) => {
return feature.properties.adcode === this.hoveredAdcode
? "#ffaa00"
: "rgba(120, 190, 255, 0.2)";
},
sideTopColor: (index, feature) => {
return feature.properties.adcode === this.hoveredAdcode
? "#ffaa00"
: "rgba(120, 190, 255, 0.3)";
},
sideBottomColor: (index, feature) => {
return feature.properties.adcode === this.hoveredAdcode
? "#884400"
: "#00396e";
},
height: 50000,
altitude: 0,
shininess: 0,
specular: "#000000",
});
this.polygonLayer.setStyle(getStyle());
this.loca.add(this.polygonLayer);
// ========== 3. 顶部边界线 ==========
this.topLineLayer = new Loca.LineLayer({
zIndex: 3,
opacity: 1,
visible: true,
zooms: [2, 22],
});
this.topLineLayer.setSource(lineSource);
const getTopLineStyle = () => ({
color: "#E0FFFF",
width: 1,
altitude: 50000,
lineWidth: 1,
});
this.topLineLayer.setStyle(getTopLineStyle());
this.loca.add(this.topLineLayer);
//鼠标移动到市,该市颜色高亮
let lastAdcode = null;
this.map.on("mousemove", (e) => {
const lnglat = e.lnglat;
let foundAdcode = null;
for (let i = 0; i < this.geoFeatures.length; i++) {
const feature = this.geoFeatures[i];
const geom = feature.geometry;
let rings = [];
if (geom.type === "MultiPolygon") {
geom.coordinates.forEach((polygon) => rings.push(...polygon));
} else if (geom.type === "Polygon") {
rings = geom.coordinates;
}
for (let j = 0; j < rings.length; j++) {
if (this.AMapIns.GeometryUtil.isPointInRing(lnglat, rings[j])) {
foundAdcode = feature.properties.adcode;
break;
}
}
if (foundAdcode) break;
}
if (lastAdcode !== foundAdcode) {
lastAdcode = foundAdcode;
this.hoveredAdcode = foundAdcode;
this.polygonLayer.setStyle(getStyle());
this.topLineLayer.setStyle(getTopLineStyle());
this.map.setDefaultCursor(foundAdcode ? "pointer" : "default");
}
});
this.map.on("mouseout", () => {
if (lastAdcode !== null) {
lastAdcode = null;
this.hoveredAdcode = null;
this.polygonLayer.setStyle(getStyle());
this.topLineLayer.setStyle(getTopLineStyle());
this.map.setDefaultCursor("default");
}
});
this.addCityAndRippleMarkers();
this.loca.animate.start();
},
addCityAndRippleMarkers() {
// 1. 统一的城市数据源
const cities = [
{ name: "太原", lng: 112.55, lat: 37.87, isRipple: true }, // ★ 标记需要涟漪的城市
{ name: "大同", lng: 113.17, lat: 40.09, isRipple: true }, // ★ 标记需要涟漪的城市
{ name: "朔州", lng: 112.44, lat: 39.33 },
{ name: "忻州", lng: 112.73, lat: 38.43 },
{ name: "吕梁", lng: 111.83, lat: 37.53 },
{ name: "晋中", lng: 112.75, lat: 37.68 },
{ name: "阳泉", lng: 113.57, lat: 37.87 },
{ name: "长治", lng: 113.13, lat: 36.19 },
{ name: "晋城", lng: 112.85, lat: 35.5 },
{ name: "临汾", lng: 111.52, lat: 36.09 },
{ name: "运城", lng: 110.99, lat: 35.02 },
];
// 文字偏移量映射
const offsetMap = {
大同: [20, -30],
朔州: [0, -40],
忻州: [-20, -50],
吕梁: [-50, -50],
太原: [-20, -30],
晋中: [20, -10],
};
const defaultOffset = [0, -30];
// 2. 清除旧标记
this.cityMarkers.forEach((marker) => marker.setMap(null));
this.cityMarkers = [];
if (this.rippleMarkers && this.rippleMarkers.length) {
this.map.remove(this.rippleMarkers);
}
this.rippleMarkers = [];
// 3. 一次遍历,生成文字和涟漪
cities.forEach((city) => {
const position = [city.lng, city.lat];
// --- 生成文字 Marker ---
const currentOffset = offsetMap[city.name] || defaultOffset;
const textMarker = new AMap.Marker({
position: position,
title: city.name,
content: `<div class="city-marker" style="color: white; font-size: 12px; pointer-events: none;">${city.name}</div>`,
offset: new AMap.Pixel(currentOffset[0], currentOffset[1]),
zIndex: 110, // 文字层级略高于涟漪
});
textMarker.setMap(this.map);
this.cityMarkers.push(textMarker);
// --- 生成涟漪 Marker (仅 isRipple 为 true 的城市) ---
if (city.isRipple) {
const rippleMarker = new AMap.Marker({
position: position,
content: `
<div class="ripple-container">
<div class="ripple-core"></div>
<div class="ripple-wave ripple-wave-1"></div>
<div class="ripple-wave ripple-wave-2"></div>
</div>
`,
offset: new AMap.Pixel(-15, -15), // 涟漪点的偏移
anchor: "center",
zIndex: 100, // 涟漪层级低于文字
});
this.map.add(rippleMarker);
this.rippleMarkers.push(rippleMarker);
}
});
},
},
};
</script>
<style scoped>
.map-container {
width: 100%;
height: 100vh;
background-color: transparent;
overflow: hidden;
background-image: url(~@/assets/images/icon_screen.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.amap-container {
background-image: none !important;
}
.map-wrapper {
width: 100%;
height: 100%;
}
:deep(.amap-logo),
:deep(.amap-copyright) {
display: none !important;
}
/* ★ 关键:强制高德地图的 Marker 外层容器穿透鼠标事件 */
:deep(.amap-marker) {
pointer-events: none !important;
}
/* ========== ★★★ 涟漪点 CSS 动画样式 ★★★ ========== */
/* 必须使用 :deep() 穿透,因为高德 Marker 是动态挂载的 DOM */
:deep(.ripple-container) {
width: 20px;
height: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
/* 中心实心点 */
:deep(.ripple-core) {
width: 8px;
height: 8px;
background: #00ffff; /* 涟漪中心颜色 */
border-radius: 50%;
box-shadow: 0 0 10px #00ffff; /* 发光效果 */
z-index: 2;
}
/* 涟漪圈公共样式 */
:deep(.ripple-wave) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
border: 2px solid #00ffff; /* 涟漪颜色 */
border-radius: 50%;
opacity: 0;
animation: ripple-expand 2s ease-out infinite;
}
/* 第二个圈延迟0.5s,产生交错感 */
:deep(.ripple-wave-2) {
animation-delay: 0.5s;
}
/* 涟漪扩散动画 */
@keyframes ripple-expand {
0% {
width: 10px;
height: 10px;
opacity: 0.8;
}
100% {
width: 30px;
height: 30px;
opacity: 0;
}
}
</style>
页面中引用:threeDimensional_ripples_page.vue
html
<template>
<div class="large-screen">
<Shanxi3DRipplesMap />
<!-- <div id="amap-container" class="map-wrapper"></div> -->
</div>
</template>
<script>
import Shanxi3DRipplesMap from "@/views/twoDimensional/components/Shanxi3DRipplesMap.vue";
export default {
name: "ThreeDimensionalRipplesPage",
components: { Shanxi3DRipplesMap },
mounted() {
// ★ 关键 2:组件挂载时,添加 PC 端缩放拦截监听
this.$nextTick(() => {
// 绑定事件,注意 { passive: false } 必须加,否则无法阻止默认行为
// document.addEventListener("wheel", this.handleWheel, { passive: false });
// document.addEventListener("keydown", this.handleKeydown);
});
},
beforeDestroy() {
// ★ 关键 3:组件销毁时,必须移除监听,否则会影响其他页面!!!
// document.removeEventListener("wheel", this.handleWheel);
// document.removeEventListener("keydown", this.handleKeydown);
},
methods: {
/** 拦截 Ctrl + 鼠标滚轮 缩放 */
// handleWheel(e) {
// if (e.ctrlKey || e.metaKey) {
// e.preventDefault();
// }
// },
/** 拦截 Ctrl + +/- 以及 Ctrl + 0 缩放 */
// handleKeydown(e) {
// // metaKey 是 Mac 的 Command 键
// if (
// (e.ctrlKey || e.metaKey) &&
// (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0")
// ) {
// e.preventDefault();
// }
// },
},
};
</script>
<style scoped>
.large-screen {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
/* background-color: #02112e; */
background-image: url(~@/assets/images/icon_screen.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.map-wrapper {
width: 100%;
height: 100%;
}
</style>
3D+动态飞线+动态呼吸点(支持大量)

地图组件:Shanxi3DFlyinglineMap.vue
html
<template>
<div class="map-container">
<div id="amap-container" class="map-wrapper"></div>
</div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import {
AMAP_KEY,
AMAP_SECRET,
AMAP_VERSION,
MAP_CENTER,
MAP_DEFAULT_ZOOM,
MAP_PITCH,
MAP_ROTATE,
} from "@/config/params.js";
export default {
name: "ShanXi3DFlyLineMap",
data() {
return {
AMapIns: null,
map: null,
loca: null,
polygonLayer: null,
topLineLayer: null,
pulseLinkLayer: null,
scatterLayers: [], // ★ 存储所有 ScatterLayer 实例
cityMarkers: [],
hoveredAdcode: null,
geoFeatures: [],
};
},
mounted() {
this.initMap();
},
beforeDestroy() {
if (this.cityMarkers && this.cityMarkers.length) {
this.map.remove(this.cityMarkers);
}
if (this.scatterLayers && this.scatterLayers.length) {
this.scatterLayers.forEach((layer) => {
this.loca.remove(layer);
});
}
if (this.loca) {
this.loca.animate.stop();
this.loca.destroy();
}
if (this.map) {
this.map.destroy();
}
},
methods: {
initMap() {
window._AMapSecurityConfig = {
securityJsCode: AMAP_SECRET,
};
AMapLoader.load({
key: AMAP_KEY,
version: AMAP_VERSION,
Loca: { version: AMAP_VERSION },
plugins: ["AMap.TileLayer.Satellite", "AMap.TileLayer.RoadNet"],
})
.then((AMap) => {
this.AMapIns = AMap;
this.map = new AMap.Map("amap-container", {
viewMode: "3D",
pitch: MAP_PITCH,
rotation: MAP_ROTATE,
zoom: MAP_DEFAULT_ZOOM,
center: MAP_CENTER,
showLabel: false,
showBuildings: false,
mapStyle: "amap://styles/a2a9b46da661bd97c1b6b028ae9e5ee7",
rotateEnable: false,
pitchEnable: false,
zoomEnable: false,
dragEnable: false,
backgroundColor: "rgba(0,0,0,0)",
});
this.loca = new Loca.Container({ map: this.map });
this.loadShanxiData();
})
.catch((e) => {
console.error("高德地图加载失败:", e);
});
},
async loadShanxiData() {
try {
const res = await fetch(
"https://geo.datav.aliyun.com/areas_v3/bound/140000_full.json"
);
const data = await res.json();
let maskCoords = [];
data.features.forEach((feature) => {
const geom = feature.geometry;
if (geom.type === "Polygon") {
maskCoords.push(geom.coordinates);
} else if (geom.type === "MultiPolygon") {
maskCoords.push(...geom.coordinates);
}
});
this.map.setMask(maskCoords);
const lineFeatures = [];
data.features.forEach((feature) => {
const geom = feature.geometry;
const coords = geom.coordinates;
let rings = [];
if (geom.type === "MultiPolygon") {
coords.forEach((polygon) => {
rings.push(...polygon);
});
} else if (geom.type === "Polygon") {
rings = coords;
}
rings.forEach((ring) => {
lineFeatures.push({
type: "Feature",
properties: feature.properties,
geometry: {
type: "LineString",
coordinates: ring,
},
});
});
});
const lineData = { type: "FeatureCollection", features: lineFeatures };
this.renderShanxi3D(data, lineData);
} catch (error) {
console.error("加载 GeoJSON 数据失败:", error);
}
},
renderShanxi3D(polygonGeoJson, lineGeoJson) {
this.geoFeatures = polygonGeoJson.features;
const polygonSource = new Loca.GeoJSONSource({ data: polygonGeoJson });
const lineSource = new Loca.GeoJSONSource({ data: lineGeoJson });
if (this.polygonLayer) this.loca.remove(this.polygonLayer);
if (this.topLineLayer) this.loca.remove(this.topLineLayer);
if (this.pulseLinkLayer) this.loca.remove(this.pulseLinkLayer);
// ========== 2. 3D 拉伸区块 ==========
this.polygonLayer = new Loca.PolygonLayer({
zIndex: 1,
opacity: 1,
visible: true,
zooms: [2, 22],
evented: true,
});
this.polygonLayer.setSource(polygonSource);
const getStyle = () => ({
topColor: (index, feature) => {
return feature.properties.adcode === this.hoveredAdcode
? "#ffaa00"
: "rgba(120, 190, 255, 0.2)";
},
sideTopColor: (index, feature) => {
return feature.properties.adcode === this.hoveredAdcode
? "#ffaa00"
: "rgba(120, 190, 255, 0.3)";
},
sideBottomColor: (index, feature) => {
return feature.properties.adcode === this.hoveredAdcode
? "#884400"
: "#00396e";
},
height: 50000,
altitude: 0,
shininess: 0,
specular: "#000000",
});
this.polygonLayer.setStyle(getStyle());
this.loca.add(this.polygonLayer);
// ========== 3. 顶部边界线 ==========
this.topLineLayer = new Loca.LineLayer({
zIndex: 3,
opacity: 1,
visible: true,
zooms: [2, 22],
});
this.topLineLayer.setSource(lineSource);
const getTopLineStyle = () => ({
color: "#E0FFFF",
width: 1,
altitude: 50000,
lineWidth: 1,
});
this.topLineLayer.setStyle(getTopLineStyle());
this.loca.add(this.topLineLayer);
//鼠标移动到市,该市颜色高亮
let lastAdcode = null;
this.map.on("mousemove", (e) => {
const lnglat = e.lnglat;
let foundAdcode = null;
for (let i = 0; i < this.geoFeatures.length; i++) {
const feature = this.geoFeatures[i];
const geom = feature.geometry;
let rings = [];
if (geom.type === "MultiPolygon") {
geom.coordinates.forEach((polygon) => rings.push(...polygon));
} else if (geom.type === "Polygon") {
rings = geom.coordinates;
}
for (let j = 0; j < rings.length; j++) {
if (this.AMapIns.GeometryUtil.isPointInRing(lnglat, rings[j])) {
foundAdcode = feature.properties.adcode;
break;
}
}
if (foundAdcode) break;
}
if (lastAdcode !== foundAdcode) {
lastAdcode = foundAdcode;
this.hoveredAdcode = foundAdcode;
this.polygonLayer.setStyle(getStyle());
this.topLineLayer.setStyle(getTopLineStyle());
this.map.setDefaultCursor(foundAdcode ? "pointer" : "default");
}
});
this.map.on("mouseout", () => {
if (lastAdcode !== null) {
lastAdcode = null;
this.hoveredAdcode = null;
this.polygonLayer.setStyle(getStyle());
this.topLineLayer.setStyle(getTopLineStyle());
this.map.setDefaultCursor("default");
}
});
this.addCityMarkersAndFlyLines();
// this.addPulseLinkLines();
this.loca.animate.start();
},
// 融合 addPulseLinkLines 和 addCityMarkers 方法
// 融合 addPulseLinkLines 和 addCityMarkers 方法
addCityMarkersAndFlyLines() {
const AMap = this.AMapIns;
const centerCity = { name: "太原", lng: 112.55, lat: 37.87 };
// 呼吸点偏移映射(包含太原)
const scatterOffsetMap = {
大同: [100, 200],
朔州: [400, 400],
忻州: [100, 400],
吕梁: [-600, -100],
太原: [-400, 100], // 太原的偏移
晋中: [100, -100],
阳泉: [-150, 500],
长治: [-200, 150],
晋城: [-250, 100],
临汾: [-250, 150],
运城: [-250, 100],
};
const defaultScatterOffset = [0, -10]; // 默认呼吸点偏移
// 城市标记偏移映射
const markerOffsetMap = {
大同: [20, -30],
朔州: [0, -40],
忻州: [-20, -50],
吕梁: [-50, -50],
太原: [-10, -30],
晋中: [20, -10],
};
const defaultMarkerOffset = [0, -30];
const otherCities = [
{ name: "太原", lng: 112.55, lat: 37.87 },
{ name: "大同", lng: 113.17, lat: 40.09 },
{ name: "朔州", lng: 112.44, lat: 39.33 },
{ name: "忻州", lng: 112.73, lat: 38.43 },
{ name: "吕梁", lng: 111.83, lat: 37.53 },
{ name: "晋中", lng: 112.75, lat: 37.68 },
{ name: "阳泉", lng: 113.57, lat: 37.87 },
{ name: "长治", lng: 113.13, lat: 36.19 },
{ name: "晋城", lng: 112.85, lat: 35.5 },
{ name: "临汾", lng: 111.52, lat: 36.09 },
{ name: "运城", lng: 110.99, lat: 35.02 },
];
// 清除现有标记
this.cityMarkers.forEach((marker) => marker.setMap(null));
this.cityMarkers = [];
this.scatterLayers = []; // 清除现有呼吸点图层
// 计算太原的adjusted坐标(用于飞线终点)
const centerOffset =
scatterOffsetMap[centerCity.name] || defaultScatterOffset;
const centerAdjustedLng = centerCity.lng + centerOffset[0] * 0.001;
const centerAdjustedLat = centerCity.lat + centerOffset[1] * 0.001;
// 存储飞线特征
const lineFeatures = [];
otherCities.forEach((city) => {
const markerOffset = markerOffsetMap[city.name] || defaultMarkerOffset;
const scatterOffset =
scatterOffsetMap[city.name] || defaultScatterOffset;
// 1. 创建城市文字标记
const marker = new AMap.Marker({
map: this.map,
position: [city.lng, city.lat],
title: city.name,
content: `<div class="city-marker-text" style="color: white; font-size: 12px;pointer-events: none;">${city.name}</div>`,
offset: new AMap.Pixel(markerOffset[0], markerOffset[1]),
zIndex: 100, // 文字层级
});
// 2. 为每个城市创建 ScatterLayer 呼吸点(在文字上方)
const scatterLayer = new Loca.ScatterLayer({
loca: this.loca,
zIndex: 101, // 确保在文字之上
opacity: 1,
visible: true,
zooms: [2, 22],
});
// 应用偏移到 GeoJSONSource 的坐标
const adjustedLng = city.lng + scatterOffset[0] * 0.001; // 转换为经度偏移
const adjustedLat = city.lat + scatterOffset[1] * 0.001; // 转换为纬度偏移
const pointGeo = new Loca.GeoJSONSource({
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {
type: 1,
ratio: 0.035,
lineWidthRatio: 0.9447674418604651,
},
geometry: {
type: "Point",
coordinates: [adjustedLng, adjustedLat],
},
},
],
},
});
scatterLayer.setSource(pointGeo);
scatterLayer.setStyle({
unit: "px",
size: [30, 30],
borderWidth: 0,
texture:
"https://a.amap.com/Loca/static/loca-v2/demos/images/breath_yellow.png",
duration: 2000,
animate: true,
});
this.loca.add(scatterLayer);
this.scatterLayers.push(scatterLayer); // 存储呼吸点图层
// 3. 存储标记和呼吸点引用
this.cityMarkers.push(marker);
// 4. 创建飞线特征,使用调整后的坐标作为起点和终点
lineFeatures.push({
type: "Feature",
id: city.name,
properties: {},
geometry: {
type: "LineString",
coordinates: [
[adjustedLng, adjustedLat], // 起点使用城市的adjusted坐标
[centerAdjustedLng, centerAdjustedLat], // 终点使用太原的adjusted坐标
],
},
});
});
// 5. 创建飞线
const flyLineData = { type: "FeatureCollection", features: lineFeatures };
const flyLineSource = new Loca.GeoJSONSource({ data: flyLineData });
if (this.pulseLinkLayer) this.loca.remove(this.pulseLinkLayer);
this.pulseLinkLayer = new Loca.PulseLinkLayer({
loca: this.loca,
zIndex: 10,
opacity: 1,
visible: true,
zooms: [2, 22],
depth: false,
});
this.pulseLinkLayer.setSource(flyLineSource);
this.pulseLinkLayer.setStyle({
unit: "meter",
altitude: 60000,
height: 80000,
dash: [40000, 0, 40000, 0],
lineWidth: [3000, 3000],
speed: 100000,
flowLength: 150000,
lineColors: [
"rgb(255,228,105)",
"rgb(255,164,105)",
"rgba(1, 34, 249,1)",
],
maxHeightScale: 0.3,
headColor: "rgba(255, 255, 0, 1)",
trailColor: "rgba(255, 255,0,0)",
});
this.loca.add(this.pulseLinkLayer);
// 6. 为中心城市(太原)添加发光点(使用原始坐标,因为发光点不需要偏移)
// const centerMarker = new AMap.Marker({
// map: this.map,
// position: [centerCity.lng, centerCity.lat],
// content: `<div class="center-glow"><div class="center-core"></div></div>`,
// offset: new AMap.Pixel(-15, -15),
// zIndex: 110,
// });
// if (centerMarker.setAltitude) {
// centerMarker.setAltitude(60000);
// }
// this.cityMarkers.push(centerMarker);
},
},
};
</script>
<style scoped>
.map-container {
width: 100%;
height: 100vh;
background-color: transparent;
overflow: hidden;
background-image: url(~@/assets/images/icon_screen.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.amap-container {
background-image: none !important;
}
.map-wrapper {
width: 100%;
height: 100%;
}
:deep(.amap-logo),
:deep(.amap-copyright) {
display: none !important;
}
:deep(.amap-marker) {
pointer-events: none !important;
}
/* ========== ★ 中心城市发光效果 ★ ========== */
:deep(.center-glow) {
width: 30px;
height: 30px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.center-core) {
width: 10px;
height: 10px;
background: #00ffff;
border-radius: 50%;
box-shadow: 0 0 20px 5px rgba(0, 255, 255, 0.8);
z-index: 2;
animation: centerPulse 2s ease-in-out infinite;
}
@keyframes centerPulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.8;
}
}
</style>
页面中引用:threeDimensional_flyline_page.vue
html
<template>
<div class="large-screen">
<Shanxi3DFlyinglineMap />
<!-- <div id="amap-container" class="map-wrapper"></div> -->
</div>
</template>
<script>
import Shanxi3DFlyinglineMap from "@/views/twoDimensional/components/Shanxi3DFlyinglineMap.vue";
export default {
name: "ThreeDimensionalFlyLinePage",
components: { Shanxi3DFlyinglineMap },
mounted() {
// ★ 关键 2:组件挂载时,添加 PC 端缩放拦截监听
this.$nextTick(() => {
// 绑定事件,注意 { passive: false } 必须加,否则无法阻止默认行为
// document.addEventListener("wheel", this.handleWheel, { passive: false });
// document.addEventListener("keydown", this.handleKeydown);
});
},
beforeDestroy() {
// ★ 关键 3:组件销毁时,必须移除监听,否则会影响其他页面!!!
// document.removeEventListener("wheel", this.handleWheel);
// document.removeEventListener("keydown", this.handleKeydown);
},
methods: {
/** 拦截 Ctrl + 鼠标滚轮 缩放 */
// handleWheel(e) {
// if (e.ctrlKey || e.metaKey) {
// e.preventDefault();
// }
// },
/** 拦截 Ctrl + +/- 以及 Ctrl + 0 缩放 */
// handleKeydown(e) {
// // metaKey 是 Mac 的 Command 键
// if (
// (e.ctrlKey || e.metaKey) &&
// (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0")
// ) {
// e.preventDefault();
// }
// },
},
};
</script>
<style scoped>
.large-screen {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
/* background-color: #02112e; */
background-image: url(~@/assets/images/icon_screen.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.map-wrapper {
width: 100%;
height: 100%;
}
</style>
3D+标牌点(动态)

地图组件:Shanxi3DSignpostPointMap.vue
html
<!-- 标牌点 -->
<template>
<div class="map-container">
<div id="amap-container" class="map-wrapper"></div>
</div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import {
AMAP_KEY,
AMAP_SECRET,
AMAP_VERSION,
MAP_CENTER,
MAP_DEFAULT_ZOOM,
MAP_PITCH,
MAP_ROTATE,
} from "@/config/params.js";
export default {
name: "Shanxi3DSignpostPointMap",
data() {
return {
AMapIns: null,
map: null,
loca: null,
polygonLayer: null,
topLineLayer: null,
bottomLineLayer: null,
cityMarkers: [],
};
},
mounted() {
this.initMap();
},
beforeDestroy() {
if (this.loca) {
this.loca.animate.stop();
this.loca.destroy();
}
if (this.map) {
this.map.destroy();
}
},
methods: {
initMap() {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECRET };
AMapLoader.load({
key: AMAP_KEY,
version: AMAP_VERSION,
Loca: { version: AMAP_VERSION }, // ★ 必须引入 Loca 才能画3D
})
.then((AMap) => {
this.AMapIns = AMap;
this.map = new AMap.Map("amap-container", {
viewMode: "3D",
rotation: MAP_ROTATE,
zoom: 16.82,
pitch: 80,
rotation: 205,
center: [112.44, 39.33],
showLabel: false,
showBuildings: false,
mapStyle: "amap://styles/dark",
rotateEnable: false,
pitchEnable: false,
zoomEnable: true,
dragEnable: true,
backgroundColor: "rgba(0,0,0,0)",
fog: {
// ★ 开启雾化,增加远景纵深感
enable: true,
color: "#000000", // 雾的颜色,建议和你的大屏背景色一致
},
});
this.loca = new Loca.Container({ map: this.map });
this.addCityMarkers();
})
.catch((e) => console.error("地图加载失败:", e));
},
addCityMarkers() {
const cities = [
{ name: "太原", lng: 112.55, lat: 37.87, price: 65000, count: 92 },
{ name: "大同", lng: 113.17, lat: 40.09, price: 65000, count: 52 },
{ name: "朔州", lng: 112.44, lat: 39.33, price: 49000, count: 53 },
{ name: "忻州", lng: 112.73, lat: 38.43, price: 62000, count: 639 },
{ name: "吕梁", lng: 111.83, lat: 37.53, price: 48000, count: 651 },
{ name: "晋中", lng: 112.75, lat: 37.68, price: 55000, count: 92 },
{ name: "阳泉", lng: 113.57, lat: 37.87, price: 40000, count: 92 },
{ name: "长治", lng: 113.13, lat: 36.19, price: 50000, count: 92 },
{ name: "晋城", lng: 112.85, lat: 35.5, price: 20000, count: 92 },
{ name: "临汾", lng: 111.52, lat: 36.09, price: 10000, count: 92 },
{ name: "运城", lng: 110.99, lat: 35.02, price: 10000, count: 92 },
];
const geoJsonData = {
type: "FeatureCollection",
features: cities.map((city) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [city.lng, city.lat],
},
properties: {
name: city.name,
price: city.price, // 将 num 也传入 properties,以便后续如果需要可以根据 num 做样式映射
count: city.count,
},
})),
};
// ★ 2. 创建 Loca.GeoJSONSource 数据源
var geo = new Loca.GeoJSONSource({
data: geoJsonData,
});
// 文字主体图层
var zMarker = new Loca.ZMarkerLayer({
loca: this.loca,
zIndex: 120,
depth: false,
});
zMarker.setSource(geo);
zMarker.setStyle({
content: (i, feat) => {
var props = feat.properties;
var leftColor =
props.price < 60000 ? "rgba(0, 28, 52, 0.6)" : "rgba(33,33,33,0.6)";
var rightColor =
props.price < 60000 ? "#038684" : "rgba(172, 137, 51, 0.3)";
var borderColor =
props.price < 60000 ? "#038684" : "rgba(172, 137, 51, 1)";
return (
'<div style="width: 490px; height: 228px; padding: 0 0;">' +
'<p style="display: block; height:80px; line-height:80px;font-size:40px;background-image: linear-gradient(to right, ' +
leftColor +
"," +
leftColor +
"," +
rightColor +
",rgba(0,0,0,0.4)); border:4px solid " +
borderColor +
'; color:#fff; border-radius: 15px; text-align:center; margin:0; padding:5px;">' +
props["name"] +
": " +
props["price"] / 10000 +
'</p><span style="width: 130px; height: 130px; margin: 0 auto; display: block; background: url(https://a.amap.com/Loca/static/loca-v2/demos/images/prism_' +
(props["price"] < 60000 ? "blue" : "yellow") +
'.png);"></span></div>'
);
},
unit: "meter",
rotation: 0,
alwaysFront: true,
size: [490 / 2, 222 / 2],
altitude: 0,
});
// 浮动三角
var triangleZMarker = new Loca.ZMarkerLayer({
loca: this.loca,
zIndex: 119,
depth: false,
});
triangleZMarker.setSource(geo);
triangleZMarker.setStyle({
content: (i, feat) => {
return (
'<div style="width: 120px; height: 120px; background: url(https://a.amap.com/Loca/static/loca-v2/demos/images/triangle_' +
(feat.properties.price < 60000 ? "blue" : "yellow") +
'.png);"></div>'
);
},
unit: "meter",
rotation: 0,
alwaysFront: true,
size: [60, 60],
altitude: 15,
});
triangleZMarker.addAnimate({
key: "altitude",
value: [0, 1],
random: true,
transform: 1000,
delay: 2000,
yoyo: true,
repeat: 999999,
});
// 呼吸点 蓝色
var scatterBlue = new Loca.ScatterLayer({
loca: this.loca,
zIndex: 110,
opacity: 1,
visible: true,
zooms: [2, 22],
depth: false,
});
scatterBlue.setSource(geo);
scatterBlue.setStyle({
unit: "px",
size: function (i, feat) {
return feat.properties.price < 60000 ? [90, 90] : [0, 0];
},
texture:
"https://a.amap.com/Loca/static/loca-v2/demos/images/scan_blue.png",
altitude: 0,
duration: 2000,
animate: true,
});
// 呼吸点 金色
var scatterYellow = new Loca.ScatterLayer({
loca: this.loca,
zIndex: 110,
opacity: 1,
visible: true,
zooms: [2, 22],
depth: false,
});
scatterYellow.setSource(geo);
scatterYellow.setStyle({
unit: "px",
size: function (i, feat) {
return feat.properties.price > 60000 ? [40, 40] : [0, 0];
},
texture:
"https://a.amap.com/Loca/static/loca-v2/demos/images/scan_yellow.png",
altitude: 0,
duration: 2000,
animate: true,
});
// 启动帧
this.loca.animate.start();
},
},
};
</script>
<style scoped>
.map-container {
width: 100%;
height: 100vh;
background-color: transparent;
overflow: hidden;
}
.map-wrapper {
width: 100%;
height: 100%;
}
:deep(.amap-logo),
:deep(.amap-copyright) {
display: none !important;
}
.amap-container {
background-image: none !important;
}
</style>
页面中引用:threeDimensionalsignpoint_page.vue
html
<template>
<div class="large-screen">
<Shanxi3DSignpostPointMap />
</div>
</template>
<script>
import Shanxi3DSignpostPointMap from "@/views/twoDimensional/components/Shanxi3DSignpostPointMap.vue";
export default {
name: "ThreeDimensionalSignPointPage",
components: { Shanxi3DSignpostPointMap },
};
</script>
<style scoped>
.large-screen {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
.map-wrapper {
width: 100%;
height: 100%;
}
</style>
激光图(动态)

地图组件:Shanxi3DLaserMap.vue
html
<!-- 激光图 -->
<template>
<div class="map-container">
<div id="satellite-map" class="map-wrapper"></div>
</div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import {
AMAP_KEY,
AMAP_SECRET,
AMAP_VERSION,
MAP_CENTER,
MAP_DEFAULT_ZOOM,
MAP_PITCH,
MAP_ROTATE,
} from "@/config/params.js";
export default {
name: "Shanxi3DLaserMap",
data() {
return {
AMapIns: null,
map: null,
loca: null,
polygonLayer: null,
topLineLayer: null,
bottomLineLayer: null,
cityMarkers: [],
};
},
mounted() {
this.initMap();
},
beforeDestroy() {
if (this.loca) {
this.loca.animate.stop();
this.loca.destroy();
}
if (this.map) {
this.map.destroy();
}
},
methods: {
initMap() {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECRET };
AMapLoader.load({
key: AMAP_KEY,
version: AMAP_VERSION,
Loca: { version: AMAP_VERSION }, // ★ 必须引入 Loca 才能画3D
})
.then((AMap) => {
this.AMapIns = AMap;
// 1. 创建卫星图层 (降低一点透明度,防止太抢眼)
const satelliteLayer = new AMap.TileLayer.Satellite({
zIndex: 0,
opacity: 0.7,
});
this.map = new AMap.Map("satellite-map", {
viewMode: "3D", // ★ 核心1:必须开启3D视图
pitch: MAP_PITCH, // ★ 核心2:俯仰角,产生鸟瞰悬浮感
rotation: MAP_ROTATE, // 带一点旋转
zoom: MAP_DEFAULT_ZOOM,
center: MAP_CENTER,
showLabel: false,
showBuildings: false,
zoomEnable: false,
dragEnable: false,
backgroundColor: "rgba(0,0,0,0)",
layers: [satelliteLayer], // 底图组合
});
this.loca = new Loca.Container({ map: this.map });
this.loadShanxiData();
})
.catch((e) => console.error("地图加载失败:", e));
},
async loadShanxiData() {
try {
// ★ 请求1:获取山西省整体外轮廓数据(用于发光)
const resProvince = await fetch(
"https://geo.datav.aliyun.com/areas_v3/bound/140000.json"
);
const provinceData = await resProvince.json();
// 请求2:获取各市详细数据(用于3D区块和内部细线)
const resCity = await fetch(
"https://geo.datav.aliyun.com/areas_v3/bound/140000_full.json"
);
const cityData = await resCity.json();
// ---------- 处理省级外轮廓线数据 ----------
const provinceLineFeatures = [];
// 兼容处理:140000.json 可能返回 Feature 或 FeatureCollection
const provinceGeom =
provinceData.type === "FeatureCollection"
? provinceData.features[0].geometry
: provinceData.geometry;
let provinceRings = [];
if (provinceGeom.type === "MultiPolygon") {
provinceGeom.coordinates.forEach((polygon) =>
provinceRings.push(...polygon)
);
} else if (provinceGeom.type === "Polygon") {
provinceRings = provinceGeom.coordinates;
}
provinceRings.forEach((ring) => {
provinceLineFeatures.push({
type: "Feature",
geometry: { type: "LineString", coordinates: ring },
});
});
const provinceLineData = {
type: "FeatureCollection",
features: provinceLineFeatures,
};
// ---------- 处理市级边界线数据 ----------
const cityLineFeatures = [];
cityData.features.forEach((feature) => {
// 注意这里用的是 cityData
const geom = feature.geometry;
let rings = [];
if (geom.type === "MultiPolygon")
geom.coordinates.forEach((polygon) => rings.push(...polygon));
else if (geom.type === "Polygon") rings = geom.coordinates;
rings.forEach((ring) => {
cityLineFeatures.push({
type: "Feature",
properties: feature.properties,
geometry: { type: "LineString", coordinates: ring },
});
});
});
const cityLineData = {
type: "FeatureCollection",
features: cityLineFeatures,
};
// 传入渲染函数
this.render3DShanxi(cityData, cityLineData, provinceLineData);
} catch (error) {
console.error("数据加载失败:", error);
}
},
render3DShanxi(cityPolygonGeoJson, cityLineGeoJson, provinceLineGeoJson) {
// 数据源实例化
const polygonSource = new Loca.GeoJSONSource({
data: cityPolygonGeoJson,
});
const cityLineSource = new Loca.GeoJSONSource({ data: cityLineGeoJson });
const provinceLineSource = new Loca.GeoJSONSource({
data: provinceLineGeoJson,
}); // ★ 新增省级线数据源
// ========== 1. 3D 半透明悬浮区块 (不变) ==========
this.polygonLayer = new Loca.PolygonLayer({
zIndex: 1,
opacity: 1,
zooms: [2, 22],
});
this.polygonLayer.setSource(polygonSource);
this.polygonLayer.setStyle({
topColor: "rgba(120, 190, 255, 0.2)",
sideTopColor: "rgba(120, 190, 255, 0.3)",
sideBottomColor: "#000a14",
height: 50000,
altitude: 0,
shininess: 30,
specular: "#111111",
});
this.loca.add(this.polygonLayer);
const altitudeVal = 50000;
// ========== 2. 顶面市级细线 (不发光,仅勾勒轮廓) ==========
const cityTopLineLayer = new Loca.LineLayer({ zIndex: 5, opacity: 1 });
cityTopLineLayer.setSource(cityLineSource); // ★ 绑定市级数据
cityTopLineLayer.setStyle({
color: "rgba(0, 229, 255, 0.4)", // 暗淡的青色,不抢眼
width: 1,
altitude: altitudeVal,
lineWidth: 1,
});
this.loca.add(cityTopLineLayer);
// ========== 3. 顶面省级外轮廓发光线 (核心修改) ==========
// 第1层:最外层微光
const glow1 = new Loca.LineLayer({ zIndex: 6, opacity: 0.6 }); // 层级要高于市线
glow1.setSource(provinceLineSource); // ★ 绑定省级数据
glow1.setStyle({
color: "rgba(0, 229, 255, 0.1)",
width: 8,
altitude: altitudeVal,
lineWidth: 8,
});
this.loca.add(glow1);
// 第2层:中层光晕
const glow2 = new Loca.LineLayer({ zIndex: 7, opacity: 0.8 });
glow2.setSource(provinceLineSource); // ★ 绑定省级数据
glow2.setStyle({
color: "rgba(0, 229, 255, 0.3)",
width: 4,
altitude: altitudeVal,
lineWidth: 4,
});
this.loca.add(glow2);
// 第3层:核心高亮线
const coreLine = new Loca.LineLayer({ zIndex: 8, opacity: 1 });
coreLine.setSource(provinceLineSource); // ★ 绑定省级数据
coreLine.setStyle({
color: "#E0FFFF",
width: 1.5,
altitude: altitudeVal,
lineWidth: 1.5,
});
this.loca.add(coreLine);
// ========== 4. 底部边界线 (可以统一用市级数据,加一点微光) ==========
this.bottomLineLayer = new Loca.LineLayer({ zIndex: 2, opacity: 0.6 });
this.bottomLineLayer.setSource(cityLineSource);
this.bottomLineLayer.setStyle({
color: "#00e5ff",
width: 2,
altitude: 1,
lineWidth: 2,
});
// this.loca.add(this.bottomLineLayer);
this.addCityMarkers();
this.loca.animate.start();
},
addCityMarkers() {
const cities = [
{ name: "太原", lng: 112.55, lat: 37.87, num: 5000 },
{ name: "大同", lng: 113.17, lat: 40.09, num: 15000 },
{ name: "朔州", lng: 112.44, lat: 39.33, num: 25000 },
{ name: "忻州", lng: 112.73, lat: 38.43, num: 35000 },
{ name: "吕梁", lng: 111.83, lat: 37.53, num: 45000 },
{ name: "晋中", lng: 112.75, lat: 37.68, num: 55000 },
{ name: "阳泉", lng: 113.57, lat: 37.87, num: 4000 },
{ name: "长治", lng: 113.13, lat: 36.19, num: 50000 },
{ name: "晋城", lng: 112.85, lat: 35.5, num: 2000 },
{ name: "临汾", lng: 111.52, lat: 36.09, num: 1000 },
{ name: "运城", lng: 110.99, lat: 35.02, num: 1000 },
];
const offsetMap = {
大同: [20, -30],
朔州: [0, -40],
忻州: [-20, -50],
吕梁: [-50, -50],
太原: [-20, -30],
晋中: [20, -10],
};
const defaultOffset = [0, -30];
const pixelToLngLatFactor = 0.005; // 像素转经纬度的近似系数
// 1. 激光图层
var layer = new Loca.LaserLayer({
zIndex: 130,
opacity: 1,
visible: true,
depth: true,
zooms: [2, 22], // ★ 修复1:放宽可视缩放级别范围
});
var heightFactor = 0.2; // ★ 修复2:调小高度系数,防止冲出屏幕
// ★ 修复3:应用 offsetMap 微调经纬度
const geoJsonData = {
type: "FeatureCollection",
features: cities.map((city) => {
const offset = offsetMap[city.name] || defaultOffset;
const offsetX = offset[0];
const offsetY = offset[1];
return {
type: "Feature",
geometry: {
type: "Point",
// X(经度):向右为正;Y(纬度):屏幕向上为负像素,对应纬度加
coordinates: [
city.lng + offsetX * pixelToLngLatFactor,
city.lat - offsetY * pixelToLngLatFactor,
],
},
properties: {
name: city.name,
h: city.num,
},
};
}),
};
var pointGeo = new Loca.GeoJSONSource({ data: geoJsonData });
layer.setSource(pointGeo, {
unit: "px",
// height: (index, feat) => {
// return feat.properties.h * heightFactor;
// },
//激光高度
height: 200,
color: (index, feat) => {
//循环从颜色数组中去颜色,形成视觉上的颜色交替
return ["#FF6F47", "#4FDDC7", "#4FDDC7"][index % 3];
},
altitude: 50000,
texture:
"https://a.amap.com/Loca/static/loca-v2/demos/images/breath_red.png",
lineWidth: 5,
// 轨迹长度
trailLength: 200,
angle: 0,
duration: 2500,
// 间隔时间
interval: 1000,
repeat: Infinity,
// 延迟时间
delay: () => {
return Math.random() * 3000;
},
});
this.loca.add(layer);
// 2. 城市名称 Marker (保持不变)
this.cityMarkers.forEach((marker) => marker.setMap(null));
this.cityMarkers = [];
cities.forEach((city) => {
const currentOffset = offsetMap[city.name] || defaultOffset;
const marker = new AMap.Marker({
position: [city.lng, city.lat], // Marker 依然使用原始坐标,依靠自身的 offset 偏移
title: city.name,
content: `<div class="city-marker" style="display: flex; flex-direction: column; align-items: center; pointer-events: none;">
<span style="color: white; font-size: 12px; margin-top: 2px; white-space: nowrap; pointer-events: none;">${city.name}</span>
</div>`,
offset: new AMap.Pixel(currentOffset[0], currentOffset[1]),
});
marker.setMap(this.map);
this.cityMarkers.push(marker);
});
},
},
};
</script>
<style scoped>
.map-container {
width: 100%;
height: 100vh;
background-color: transparent;
overflow: hidden;
background-image: url(~@/assets/images/icon_screen.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* .map-container {
width: 100%;
height: 100vh;
background-color: #000;
overflow: hidden;
} */
.map-wrapper {
width: 100%;
height: 100%;
}
:deep(.amap-logo),
:deep(.amap-copyright) {
display: none !important;
}
.amap-container {
background-image: none !important;
}
/* ★ 核心4:将卫星图层去色变暗,极大增强科技感,避免实景图喧宾夺主 */
:deep(.amap-layer) {
/* filter: grayscale(100%) brightness(0.5) contrast(1.2); */
}
</style>
目前存在问题:
1、使用高德地图,展示一个省或者市的地图,在宽高上不好控制,
2、3D地图贴图功能暂未实现,且无法实现整块地图渐变效果(所以在视觉效果自定义上有缺陷,方案:之后会研究Three.js是否可以结合高德一起实现)