大屏地图实现方案之-高德(二)

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是否可以结合高德一起实现)

相关推荐
森林的尽头是阳光6 小时前
前端使用postman快速造数据
前端·javascript·vue·postman·造数·本地测试
腾讯位置服务1 天前
5月产品上新|行业潜客产品、违停状态查询API、两轮车距离矩阵3大能力全新上线!
地图·导航·两轮车·腾讯位置服务
文阿花1 天前
大屏实现方案之-高德
vue·地图·高德
Anesthesia丶2 天前
Vite + Svelte + shadcn-svelte 最小化 Demo+Vue3语法对比总结
vue·vite·svelte·shadcn-svelte
孟郎郎2 天前
TimeoutError: The operation was aborted due to timeout at new DOMException
ai·前端框架·npm·vue·pnpm·deepseek
lpd_lt2 天前
AI生成Spring Boot + Vue 3 + MySQL + MyBatis-Plus的项目实战
java·spring boot·vue·ai编程
来杯@Java3 天前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
华大哥3 天前
前后端分离实现五级行政区划树形菜单及设备查询管理
sqlite·vue·springboot
码界筑梦坊3 天前
282-基于Python的豆瓣音乐可视化分析推荐系统
开发语言·python·信息可视化·数据分析·flask·vue