{T}

实战案例

本文档提供百度地图的典型应用场景示例,帮助开发者快速实现各类地图功能。

案例1:门店地图

展示企业门店位置,支持搜索和导航。

功能需求

  • 地图展示所有门店位置
  • 点击门店显示详情
  • 支持搜索附近门店
  • 一键导航到门店

完整代码

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>门店地图</title>
  <style>
    * { margin: 0; padding: 0; }
    body { font-family: Arial, sans-serif; }
    #container { display: flex; height: 100vh; }
    #sidebar { width: 300px; background: #f5f5f5; overflow-y: auto; }
    #map { flex: 1; }
    .search-box { padding: 15px; background: #fff; border-bottom: 1px solid #eee; }
    .search-box input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
    .store-list { padding: 10px; }
    .store-item { padding: 15px; background: #fff; margin-bottom: 10px; border-radius: 4px; cursor: pointer; }
    .store-item:hover { background: #e8f4fc; }
    .store-item h3 { font-size: 14px; margin-bottom: 5px; }
    .store-item p { font-size: 12px; color: #666; }
    .store-item .distance { color: #1890ff; font-weight: bold; }
    .info-window { padding: 10px; min-width: 200px; }
    .info-window h3 { margin-bottom: 10px; }
    .info-window p { margin-bottom: 5px; font-size: 13px; }
    .info-window .btn { 
      display: inline-block; 
      padding: 5px 15px; 
      background: #1890ff; 
      color: #fff; 
      border-radius: 4px; 
      cursor: pointer; 
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="sidebar">
      <div class="search-box">
        <input type="text" id="searchInput" placeholder="搜索门店名称或地址">
      </div>
      <div class="store-list" id="storeList"></div>
    </div>
    <div id="map"></div>
  </div>

  <script src="https://api.map.baidu.com/api?v=3.0&ak=您的密钥"></script>
  <script>
    var stores = [
      { id: 1, name: '百度大厦店', address: '北京市海淀区上地十街10号', lng: 116.308, lat: 40.056, phone: '010-12345678' },
      { id: 2, name: '中关村店', address: '北京市海淀区中关村大街1号', lng: 116.316, lat: 39.983, phone: '010-87654321' },
      { id: 3, name: '西单店', address: '北京市西城区西单北大街', lng: 116.373, lat: 39.913, phone: '010-11112222' },
      { id: 4, name: '国贸店', address: '北京市朝阳区建国门外大街', lng: 116.461, lat: 39.909, phone: '010-33334444' },
      { id: 5, name: '望京店', address: '北京市朝阳区望京街道', lng: 116.481, lat: 40.001, phone: '010-55556666' }
    ];

    var map = new BMap.Map('map');
    map.centerAndZoom(new BMap.Point(116.404, 39.915), 12);
    map.enableScrollWheelZoom(true);
    map.addControl(new BMap.NavigationControl());

    var markers = [];
    var currentInfoWindow = null;

    stores.forEach(function(store) {
      var point = new BMap.Point(store.lng, store.lat);
      var marker = new BMap.Marker(point);
      marker.storeData = store;
      map.addOverlay(marker);
      markers.push(marker);

      marker.addEventListener('click', function() {
        showStoreInfo(store, marker);
      });
    });

    function showStoreInfo(store, marker) {
      if (currentInfoWindow) {
        map.closeInfoWindow();
      }

      var content = `
        <div class="info-window">
          <h3>${store.name}</h3>
          <p>地址:${store.address}</p>
          <p>电话:${store.phone}</p>
          <div class="btn" onclick="navigateTo(${store.lng}, ${store.lat})">导航到店</div>
        </div>
      `;

      var infoWindow = new BMap.InfoWindow(content);
      marker.openInfoWindow(infoWindow);
      currentInfoWindow = infoWindow;
    }

    function navigateTo(lng, lat) {
      var geolocation = new BMap.Geolocation();
      geolocation.getCurrentPosition(function(result) {
        if (this.getStatus() === BMAP_STATUS_SUCCESS) {
          var start = result.point;
          var end = new BMap.Point(lng, lat);
          
          var driving = new BMap.DrivingRoute(map, {
            renderOptions: { map: map, autoViewport: true }
          });
          driving.search(start, end);
        } else {
          alert('无法获取您的位置');
        }
      });
    }

    function renderStoreList(filterText) {
      var list = document.getElementById('storeList');
      var filteredStores = stores.filter(function(store) {
        if (!filterText) return true;
        return store.name.includes(filterText) || store.address.includes(filterText);
      });

      list.innerHTML = filteredStores.map(function(store) {
        return `
          <div class="store-item" onclick="focusStore(${store.id})">
            <h3>${store.name}</h3>
            <p>${store.address}</p>
            <p>电话:${store.phone}</p>
          </div>
        `;
      }).join('');
    }

    function focusStore(id) {
      var store = stores.find(function(s) { return s.id === id; });
      if (store) {
        var point = new BMap.Point(store.lng, store.lat);
        map.centerAndZoom(point, 15);
        
        var marker = markers.find(function(m) { return m.storeData.id === id; });
        if (marker) {
          showStoreInfo(store, marker);
        }
      }
    }

    document.getElementById('searchInput').addEventListener('input', function(e) {
      renderStoreList(e.target.value);
    });

    renderStoreList();
  </script>
</body>
</html>

案例2:Vue3组件封装

使用Vue3封装可复用的地图组件。

BaiduMap组件

Vue SFC
<template>
  <div class="baidu-map" ref="mapContainer"></div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, watch, defineProps, defineEmits } from 'vue';

const props = defineProps({
  ak: {
    type: String,
    required: true
  },
  center: {
    type: Object,
    default: () => ({ lng: 116.404, lat: 39.915 })
  },
  zoom: {
    type: Number,
    default: 15
  },
  enableScrollWheelZoom: {
    type: Boolean,
    default: true
  },
  mapType: {
    type: String,
    default: 'normal' // normal, satellite, hybrid
  }
});

const emit = defineEmits(['ready', 'click', 'zoomend', 'moveend']);

const mapContainer = ref(null);
const map = ref(null);
const isReady = ref(false);

// 加载百度地图API
const loadMapAPI = () => {
  return new Promise((resolve) => {
    if (window.BMap) {
      resolve();
      return;
    }

    window.initBaiduMap = () => {
      resolve();
    };

    const script = document.createElement('script');
    script.src = `https://api.map.baidu.com/api?v=3.0&ak=${props.ak}&callback=initBaiduMap`;
    script.async = true;
    document.head.appendChild(script);
  });
};

// 初始化地图
const initMap = async () => {
  await loadMapAPI();

  map.value = new BMap.Map(mapContainer.value, {
    enableMapClick: false
  });

  const point = new BMap.Point(props.center.lng, props.center.lat);
  map.value.centerAndZoom(point, props.zoom);

  if (props.enableScrollWheelZoom) {
    map.value.enableScrollWheelZoom(true);
  }

  // 设置地图类型
  setMapType(props.mapType);

  // 绑定事件
  map.value.addEventListener('click', (e) => {
    emit('click', {
      lng: e.point.lng,
      lat: e.point.lat,
      pixel: { x: e.pixel.x, y: e.pixel.y }
    });
  });

  map.value.addEventListener('zoomend', () => {
    emit('zoomend', map.value.getZoom());
  });

  map.value.addEventListener('moveend', () => {
    const center = map.value.getCenter();
    emit('moveend', { lng: center.lng, lat: center.lat });
  });

  isReady.value = true;
  emit('ready', map.value);
};

// 设置地图类型
const setMapType = (type) => {
  const typeMap = {
    normal: BMAP_NORMAL_MAP,
    satellite: BMAP_SATELLITE_MAP,
    hybrid: BMAP_HYBRID_MAP
  };
  map.value?.setMapType(typeMap[type] || BMAP_NORMAL_MAP);
};

// 监听属性变化
watch(() => props.center, (newCenter) => {
  if (map.value && newCenter) {
    const point = new BMap.Point(newCenter.lng, newCenter.lat);
    map.value.setCenter(point);
  }
}, { deep: true });

watch(() => props.zoom, (newZoom) => {
  map.value?.setZoom(newZoom);
});

watch(() => props.mapType, (newType) => {
  setMapType(newType);
});

// 暴露方法
const addMarker = (lng, lat, options = {}) => {
  if (!map.value) return null;

  const point = new BMap.Point(lng, lat);
  const marker = new BMap.Marker(point, options);
  map.value.addOverlay(marker);
  return marker;
};

const removeOverlay = (overlay) => {
  map.value?.removeOverlay(overlay);
};

const setCenter = (lng, lat) => {
  if (map.value) {
    const point = new BMap.Point(lng, lat);
    map.value.setCenter(point);
  }
};

const getCenter = () => {
  if (!map.value) return null;
  const center = map.value.getCenter();
  return { lng: center.lng, lat: center.lat };
};

const fitView = (points, options = {}) => {
  if (!map.value || !points.length) return;
  const bPoints = points.map(p => new BMap.Point(p.lng, p.lat));
  map.value.setViewport(bPoints, options);
};

defineExpose({
  map,
  addMarker,
  removeOverlay,
  setCenter,
  getCenter,
  fitView
});

onMounted(() => {
  initMap();
});

onBeforeUnmount(() => {
  map.value = null;
});
</script>

<style scoped>
.baidu-map {
  width: 100%;
  height: 100%;
}
</style>

使用示例

Vue SFC
<template>
  <div class="map-page">
    <BaiduMap
      ref="mapRef"
      :ak="ak"
      :center="center"
      :zoom="zoom"
      @ready="onMapReady"
      @click="onMapClick"
    />
    <div class="controls">
      <button @click="addRandomMarker">添加标注</button>
      <button @click="clearMarkers">清除标注</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import BaiduMap from './components/BaiduMap.vue';

const ak = '您的密钥';
const center = ref({ lng: 116.404, lat: 39.915 });
const zoom = ref(15);
const mapRef = ref(null);
const markers = ref([]);

const onMapReady = (map) => {
  console.log('地图已就绪', map);
};

const onMapClick = (e) => {
  console.log('点击坐标:', e.lng, e.lat);
};

const addRandomMarker = () => {
  const lng = 116.404 + (Math.random() - 0.5) * 0.1;
  const lat = 39.915 + (Math.random() - 0.5) * 0.1;
  const marker = mapRef.value.addMarker(lng, lat, {
    enableDragging: true
  });
  markers.value.push(marker);
};

const clearMarkers = () => {
  markers.value.forEach(marker => {
    mapRef.value.removeOverlay(marker);
  });
  markers.value = [];
};
</script>

<style scoped>
.map-page {
  width: 100%;
  height: 100vh;
  position: relative;
}
.controls {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 100;
}
.controls button {
  margin: 5px;
  padding: 8px 16px;
}
</style>

案例3:React组件封装

使用React封装地图组件。

BaiduMap组件

tsx
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';

interface Point {
  lng: number;
  lat: number;
}

interface BaiduMapProps {
  ak: string;
  center?: Point;
  zoom?: number;
  enableScrollWheelZoom?: boolean;
  mapType?: 'normal' | 'satellite' | 'hybrid';
  onReady?: (map: any) => void;
  onClick?: (point: Point & { pixel: { x: number; y: number } }) => void;
  onZoomend?: (zoom: number) => void;
  onMoveend?: (center: Point) => void;
  style?: React.CSSProperties;
  className?: string;
}

export interface BaiduMapRef {
  map: any;
  addMarker: (lng: number, lat: number, options?: any) => any;
  removeOverlay: (overlay: any) => void;
  setCenter: (lng: number, lat: number) => void;
  getCenter: () => Point | null;
  fitView: (points: Point[], options?: any) => void;
}

const BaiduMap = forwardRef<BaiduMapRef, BaiduMapProps>((props, ref) => {
  const {
    ak,
    center = { lng: 116.404, lat: 39.915 },
    zoom = 15,
    enableScrollWheelZoom = true,
    mapType = 'normal',
    onReady,
    onClick,
    onZoomend,
    onMoveend,
    style,
    className
  } = props;

  const containerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<any>(null);
  const [isReady, setIsReady] = useState(false);

  // 加载百度地图API
  const loadMapAPI = (): Promise<void> => {
    return new Promise((resolve) => {
      if ((window as any).BMap) {
        resolve();
        return;
      }

      (window as any).initBaiduMap = () => {
        resolve();
      };

      const script = document.createElement('script');
      script.src = `https://api.map.baidu.com/api?v=3.0&ak=${ak}&callback=initBaiduMap`;
      script.async = true;
      document.head.appendChild(script);
    });
  };

  // 初始化地图
  useEffect(() => {
    const initMap = async () => {
      await loadMapAPI();

      const BMap = (window as any).BMap;
      mapRef.current = new BMap.Map(containerRef.current, {
        enableMapClick: false
      });

      const point = new BMap.Point(center.lng, center.lat);
      mapRef.current.centerAndZoom(point, zoom);

      if (enableScrollWheelZoom) {
        mapRef.current.enableScrollWheelZoom(true);
      }

      // 绑定事件
      if (onClick) {
        mapRef.current.addEventListener('click', (e: any) => {
          onClick({
            lng: e.point.lng,
            lat: e.point.lat,
            pixel: { x: e.pixel.x, y: e.pixel.y }
          });
        });
      }

      if (onZoomend) {
        mapRef.current.addEventListener('zoomend', () => {
          onZoomend(mapRef.current.getZoom());
        });
      }

      if (onMoveend) {
        mapRef.current.addEventListener('moveend', () => {
          const c = mapRef.current.getCenter();
          onMoveend({ lng: c.lng, lat: c.lat });
        });
      }

      setIsReady(true);
      onReady?.(mapRef.current);
    };

    initMap();

    return () => {
      mapRef.current = null;
    };
  }, []);

  // 监听center变化
  useEffect(() => {
    if (mapRef.current && center) {
      const BMap = (window as any).BMap;
      const point = new BMap.Point(center.lng, center.lat);
      mapRef.current.setCenter(point);
    }
  }, [center.lng, center.lat]);

  // 监听zoom变化
  useEffect(() => {
    mapRef.current?.setZoom(zoom);
  }, [zoom]);

  // 暴露方法
  useImperativeHandle(ref, () => ({
    map: mapRef.current,
    addMarker: (lng: number, lat: number, options = {}) => {
      if (!mapRef.current) return null;
      const BMap = (window as any).BMap;
      const point = new BMap.Point(lng, lat);
      const marker = new BMap.Marker(point, options);
      mapRef.current.addOverlay(marker);
      return marker;
    },
    removeOverlay: (overlay: any) => {
      mapRef.current?.removeOverlay(overlay);
    },
    setCenter: (lng: number, lat: number) => {
      if (mapRef.current) {
        const BMap = (window as any).BMap;
        const point = new BMap.Point(lng, lat);
        mapRef.current.setCenter(point);
      }
    },
    getCenter: () => {
      if (!mapRef.current) return null;
      const c = mapRef.current.getCenter();
      return { lng: c.lng, lat: c.lat };
    },
    fitView: (points: Point[], options = {}) => {
      if (!mapRef.current || !points.length) return;
      const BMap = (window as any).BMap;
      const bPoints = points.map(p => new BMap.Point(p.lng, p.lat));
      mapRef.current.setViewport(bPoints, options);
    }
  }));

  return (
    <div
      ref={containerRef}
      className={className}
      style={{ width: '100%', height: '100%', ...style }}
    />
  );
});

BaiduMap.displayName = 'BaiduMap';

export default BaiduMap;

使用示例

tsx
import React, { useRef, useState } from 'react';
import BaiduMap, { BaiduMapRef } from './components/BaiduMap';

const MapPage: React.FC = () => {
  const mapRef = useRef<BaiduMapRef>(null);
  const [markers, setMarkers] = useState<any[]>([]);

  const handleMapReady = (map: any) => {
    console.log('地图已就绪', map);
  };

  const handleMapClick = (point: any) => {
    console.log('点击坐标:', point.lng, point.lat);
  };

  const addRandomMarker = () => {
    if (!mapRef.current) return;
    const lng = 116.404 + (Math.random() - 0.5) * 0.1;
    const lat = 39.915 + (Math.random() - 0.5) * 0.1;
    const marker = mapRef.current.addMarker(lng, lat, {
      enableDragging: true
    });
    setMarkers([...markers, marker]);
  };

  const clearMarkers = () => {
    markers.forEach(marker => {
      mapRef.current?.removeOverlay(marker);
    });
    setMarkers([]);
  };

  return (
    <div style={{ width: '100%', height: '100vh', position: 'relative' }}>
      <BaiduMap
        ref={mapRef}
        ak="您的密钥"
        center={{ lng: 116.404, lat: 39.915 }}
        zoom={15}
        onReady={handleMapReady}
        onClick={handleMapClick}
      />
      <div style={{ position: 'absolute', top: 10, right: 10, zIndex: 100 }}>
        <button onClick={addRandomMarker} style={{ margin: 5, padding: '8px 16px' }}>
          添加标注
        </button>
        <button onClick={clearMarkers} style={{ margin: 5, padding: '8px 16px' }}>
          清除标注
        </button>
      </div>
    </div>
  );
};

export default MapPage;

案例4:TypeScript完整封装

完整的TypeScript地图服务类封装。

typescript
// types/baidu-map.d.ts
declare namespace BMap {
  class Map {
    constructor(container: string | HTMLElement, opts?: MapOptions);
    centerAndZoom(center: Point, zoom: number): void;
    setCenter(center: Point): void;
    getCenter(): Point;
    setZoom(zoom: number): void;
    getZoom(): number;
    addOverlay(overlay: Overlay): void;
    removeOverlay(overlay: Overlay): void;
    clearOverlays(): void;
    getOverlays(): Overlay[];
    addControl(control: Control): void;
    removeControl(control: Control): void;
    getBounds(): Bounds;
    setViewport(points: Point[], viewportOptions?: ViewportOptions): void;
    enableScrollWheelZoom(): void;
    disableScrollWheelZoom(): void;
    addEventListener(event: string, handler: Function): void;
    removeEventListener(event: string, handler: Function): void;
  }

  class Point {
    constructor(lng: number, lat: number);
    lng: number;
    lat: number;
  }

  class Marker {
    constructor(point: Point, opts?: MarkerOptions);
    setPosition(position: Point): void;
    getPosition(): Point;
    setIcon(icon: Icon): void;
    addEventListener(event: string, handler: Function): void;
  }

  // ... 其他类型定义
}

// BaiduMapService.ts
interface MapConfig {
  ak: string;
  container: string | HTMLElement;
  center?: { lng: number; lat: number };
  zoom?: number;
}

interface MarkerOptions {
  enableDragging?: boolean;
  icon?: string;
  offset?: { x: number; y: number };
}

interface Point {
  lng: number;
  lat: number;
}

class BaiduMapService {
  private map: BMap.Map | null = null;
  private ak: string;
  private markers: Map<string, BMap.Marker> = new Map();
  private listeners: Map<string, Function[]> = new Map();

  constructor(config: MapConfig) {
    this.ak = config.ak;
    this.init(config);
  }

  private async init(config: MapConfig): Promise<void> {
    await this.loadAPI();
    
    this.map = new BMap.Map(config.container, {
      enableMapClick: false
    });

    const center = config.center || { lng: 116.404, lat: 39.915 };
    const point = new BMap.Point(center.lng, center.lat);
    this.map.centerAndZoom(point, config.zoom || 15);
    this.map.enableScrollWheelZoom();

    this.emit('ready', this.map);
  }

  private loadAPI(): Promise<void> {
    return new Promise((resolve) => {
      if ((window as any).BMap) {
        resolve();
        return;
      }

      (window as any).initBaiduMap = () => resolve();

      const script = document.createElement('script');
      script.src = `https://api.map.baidu.com/api?v=3.0&ak=${this.ak}&callback=initBaiduMap`;
      script.async = true;
      document.head.appendChild(script);
    });
  }

  // 添加标注
  addMarker(id: string, point: Point, options?: MarkerOptions): BMap.Marker | null {
    if (!this.map) return null;

    const marker = new BMap.Marker(
      new BMap.Point(point.lng, point.lat),
      {
        enableDragging: options?.enableDragging || false
      }
    );

    this.map.addOverlay(marker);
    this.markers.set(id, marker);

    return marker;
  }

  // 移除标注
  removeMarker(id: string): void {
    const marker = this.markers.get(id);
    if (marker && this.map) {
      this.map.removeOverlay(marker);
      this.markers.delete(id);
    }
  }

  // 设置中心点
  setCenter(point: Point): void {
    if (this.map) {
      this.map.setCenter(new BMap.Point(point.lng, point.lat));
    }
  }

  // 获取中心点
  getCenter(): Point | null {
    if (!this.map) return null;
    const center = this.map.getCenter();
    return { lng: center.lng, lat: center.lat };
  }

  // 设置缩放级别
  setZoom(zoom: number): void {
    this.map?.setZoom(zoom);
  }

  // 获取缩放级别
  getZoom(): number | null {
    return this.map?.getZoom() || null;
  }

  // 适应视野
  fitView(points: Point[]): void {
    if (!this.map || !points.length) return;
    const bPoints = points.map(p => new BMap.Point(p.lng, p.lat));
    this.map.setViewport(bPoints);
  }

  // 清除所有覆盖物
  clearOverlays(): void {
    this.map?.clearOverlays();
    this.markers.clear();
  }

  // 事件系统
  on(event: string, handler: Function): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(handler);
  }

  off(event: string, handler?: Function): void {
    if (!handler) {
      this.listeners.delete(event);
    } else {
      const handlers = this.listeners.get(event);
      if (handlers) {
        const index = handlers.indexOf(handler);
        if (index > -1) {
          handlers.splice(index, 1);
        }
      }
    }
  }

  private emit(event: string, ...args: any[]): void {
    const handlers = this.listeners.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(...args));
    }
  }

  // 销毁
  destroy(): void {
    this.map?.clearOverlays();
    this.markers.clear();
    this.listeners.clear();
    this.map = null;
  }
}

export default BaiduMapService;

案例5:移动端适配

针对移动端设备的优化实现。

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>移动端地图</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    
    #map { width: 100%; height: 100%; }
    
    .mobile-controls {
      position: fixed;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      gap: 10px;
      z-index: 100;
    }
    
    .mobile-btn {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      background: #fff;
      border: none;
      box-shadow: 0 2px 10px rgba(0,0,0,0.2);
      font-size: 20px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    .search-bar {
      position: fixed;
      top: 10px;
      left: 10px;
      right: 10px;
      z-index: 100;
    }
    
    .search-bar input {
      width: 100%;
      padding: 12px 15px;
      border: none;
      border-radius: 25px;
      font-size: 16px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.2);
    }
    
    .bottom-sheet {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      background: #fff;
      border-radius: 20px 20px 0 0;
      padding: 20px;
      transform: translateY(70%);
      transition: transform 0.3s ease;
      z-index: 101;
      max-height: 70vh;
      overflow-y: auto;
    }
    
    .bottom-sheet.expanded {
      transform: translateY(0);
    }
    
    .bottom-sheet-handle {
      width: 40px;
      height: 5px;
      background: #ddd;
      border-radius: 3px;
      margin: 0 auto 15px;
    }
  </style>
</head>
<body>
  <div id="map"></div>
  
  <div class="search-bar">
    <input type="text" id="searchInput" placeholder="搜索地点">
  </div>
  
  <div class="mobile-controls">
    <button class="mobile-btn" id="locateBtn">📍</button>
    <button class="mobile-btn" id="zoomInBtn">+</button>
    <button class="mobile-btn" id="zoomOutBtn">-</button>
  </div>
  
  <div class="bottom-sheet" id="bottomSheet">
    <div class="bottom-sheet-handle"></div>
    <div id="sheetContent">
      <h3>附近地点</h3>
      <div id="placeList"></div>
    </div>
  </div>

  <script src="https://api.map.baidu.com/api?v=3.0&ak=您的密钥"></script>
  <script>
    // 移动端优化配置
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    
    // 初始化地图
    const map = new BMap.Map('map', {
      enableMapClick: false  // 禁用点击热点,提升性能
    });
    map.centerAndZoom(new BMap.Point(116.404, 39.915), 15);
    
    // 移动端优化
    if (isMobile) {
      map.enableDragging();
      map.enableInertialDragging();
      map.enablePinchToZoom();
      
      // 禁用双指缩放时的动画
      map.disableDoubleClickZoom();
    }
    
    // 定位功能
    document.getElementById('locateBtn').addEventListener('click', function() {
      const geolocation = new BMap.Geolocation();
      
      // 显示加载状态
      this.textContent = '⏳';
      
      geolocation.getCurrentPosition(function(result) {
        if (this.getStatus() === BMAP_STATUS_SUCCESS) {
          const point = result.point;
          map.centerAndZoom(point, 15);
          
          // 添加定位标注
          const marker = new BMap.Marker(point);
          map.addOverlay(marker);
          
          // 添加定位圆圈
          const circle = new BMap.Circle(point, result.accuracy, {
            strokeColor: '#1890ff',
            strokeWeight: 1,
            fillColor: '#1890ff',
            fillOpacity: 0.2
          });
          map.addOverlay(circle);
        } else {
          alert('定位失败');
        }
        
        document.getElementById('locateBtn').textContent = '📍';
      }, {
        enableHighAccuracy: true
      });
    });
    
    // 缩放控制
    document.getElementById('zoomInBtn').addEventListener('click', () => map.zoomIn());
    document.getElementById('zoomOutBtn').addEventListener('click', () => map.zoomOut());
    
    // 底部面板交互
    const bottomSheet = document.getElementById('bottomSheet');
    let startY = 0;
    let currentY = 0;
    
    bottomSheet.addEventListener('touchstart', (e) => {
      startY = e.touches[0].clientY;
    });
    
    bottomSheet.addEventListener('touchmove', (e) => {
      currentY = e.touches[0].clientY;
      const diff = startY - currentY;
      
      if (diff > 50) {
        bottomSheet.classList.add('expanded');
      } else if (diff < -50) {
        bottomSheet.classList.remove('expanded');
      }
    });
    
    // 搜索功能
    const searchInput = document.getElementById('searchInput');
    let searchTimeout;
    
    searchInput.addEventListener('input', (e) => {
      clearTimeout(searchTimeout);
      const query = e.target.value;
      
      if (!query) return;
      
      searchTimeout = setTimeout(() => {
        const local = new BMap.LocalSearch(map, {
          onSearchComplete: (results) => {
            const placeList = document.getElementById('placeList');
            placeList.innerHTML = '';
            
            for (let i = 0; i < results.getCurrentNumPois(); i++) {
              const poi = results.getPoi(i);
              const item = document.createElement('div');
              item.className = 'place-item';
              item.innerHTML = `
                <h4>${poi.title}</h4>
                <p>${poi.address}</p>
              `;
              item.addEventListener('click', () => {
                map.centerAndZoom(poi.point, 16);
                map.addOverlay(new BMap.Marker(poi.point));
                bottomSheet.classList.remove('expanded');
              });
              placeList.appendChild(item);
            }
            
            bottomSheet.classList.add('expanded');
          }
        });
        
        local.search(query);
      }, 300);
    });
    
    // 触摸事件优化
    document.addEventListener('touchmove', function(e) {
      if (e.target.id === 'map') {
        e.preventDefault();
      }
    }, { passive: false });
  </script>
</body>
</html>

案例6:轨迹回放

展示车辆或人员的移动轨迹。

功能需求

  • 加载历史轨迹数据
  • 播放/暂停轨迹动画
  • 调整播放速度
  • 显示当前位置信息

核心代码

javascript
class TrackPlayer {
  constructor(map, options = {}) {
    this.map = map;
    this.options = {
      speed: 1,
      loop: false,
      onProgress: null,
      onComplete: null,
      ...options
    };
    
    this.trackPoints = [];
    this.currentIndex = 0;
    this.isPlaying = false;
    this.polyline = null;
    this.marker = null;
    this.animationId = null;
  }

  // 加载轨迹数据
  loadTrack(points) {
    this.trackPoints = points;
    this.currentIndex = 0;

    // 绘制轨迹线
    const bPoints = points.map(p => new BMap.Point(p.lng, p.lat));
    this.polyline = new BMap.Polyline(bPoints, {
      strokeColor: '#1890ff',
      strokeWeight: 3,
      strokeOpacity: 0.8
    });
    this.map.addOverlay(this.polyline);

    // 创建移动标记
    const startIcon = new BMap.Icon('car.png', new BMap.Size(32, 32), {
      anchor: new BMap.Size(16, 16)
    });
    
    this.marker = new BMap.Marker(bPoints[0], { icon: startIcon });
    this.map.addOverlay(this.marker);

    // 调整视野
    this.map.setViewport(bPoints);
  }

  // 播放
  play() {
    if (this.isPlaying || !this.trackPoints.length) return;
    
    this.isPlaying = true;
    this.animate();
  }

  // 暂停
  pause() {
    this.isPlaying = false;
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
      this.animationId = null;
    }
  }

  // 停止
  stop() {
    this.pause();
    this.currentIndex = 0;
    if (this.marker && this.trackPoints.length) {
      const point = new BMap.Point(
        this.trackPoints[0].lng,
        this.trackPoints[0].lat
      );
      this.marker.setPosition(point);
    }
  }

  // 动画
  animate() {
    if (!this.isPlaying) return;

    if (this.currentIndex >= this.trackPoints.length) {
      if (this.options.loop) {
        this.currentIndex = 0;
      } else {
        this.isPlaying = false;
        this.options.onComplete?.();
        return;
      }
    }

    const point = this.trackPoints[this.currentIndex];
    const bPoint = new BMap.Point(point.lng, point.lat);
    
    this.marker.setPosition(bPoint);
    
    // 计算方向角度
    if (this.currentIndex < this.trackPoints.length - 1) {
      const nextPoint = this.trackPoints[this.currentIndex + 1];
      const angle = this.calculateAngle(point, nextPoint);
      this.marker.setRotation(angle);
    }

    // 回调进度
    this.options.onProgress?.({
      index: this.currentIndex,
      total: this.trackPoints.length,
      point: point
    });

    this.currentIndex++;
    
    // 根据速度调整间隔
    const interval = 50 / this.options.speed;
    
    setTimeout(() => {
      this.animationId = requestAnimationFrame(() => this.animate());
    }, interval);
  }

  // 设置速度
  setSpeed(speed) {
    this.options.speed = Math.max(0.1, Math.min(10, speed));
  }

  // 设置进度
  setProgress(progress) {
    const index = Math.floor(progress * this.trackPoints.length);
    this.currentIndex = Math.max(0, Math.min(index, this.trackPoints.length - 1));
    
    if (this.marker && this.trackPoints[this.currentIndex]) {
      const point = this.trackPoints[this.currentIndex];
      this.marker.setPosition(new BMap.Point(point.lng, point.lat));
    }
  }

  // 计算角度
  calculateAngle(from, to) {
    const dx = to.lng - from.lng;
    const dy = to.lat - from.lat;
    return Math.atan2(dx, dy) * 180 / Math.PI;
  }

  // 清理
  destroy() {
    this.stop();
    if (this.polyline) {
      this.map.removeOverlay(this.polyline);
    }
    if (this.marker) {
      this.map.removeOverlay(this.marker);
    }
    this.trackPoints = [];
  }
}

// 使用示例
const player = new TrackPlayer(map, {
  speed: 2,
  loop: true,
  onProgress: (info) => {
    console.log(`进度: ${info.index}/${info.total}`);
  },
  onComplete: () => {
    console.log('播放完成');
  }
});

// 加载轨迹数据
player.loadTrack([
  { lng: 116.399, lat: 39.910, time: '2024-01-01 08:00:00' },
  { lng: 116.400, lat: 39.911, time: '2024-01-01 08:01:00' },
  // ... 更多轨迹点
]);

// 控制播放
player.play();   // 播放
player.pause();  // 暂停
player.stop();   // 停止
player.setSpeed(3);  // 设置3倍速
player.setProgress(0.5);  // 跳转到50%

最佳实践总结

性能优化

优化项说明实现方式
海量点展示使用MassOverlay或MarkerClusterer减少DOM元素数量
批量操作频繁操作时禁用地图拖拽map.disableDragging()
事件节流处理频繁触发的事件setTimeout节流
缓存优化减少API调用次数本地缓存结果
延迟加载按需加载非必要功能BMap.loader.load()

用户体验

优化项说明
加载状态显示加载进度或骨架屏
错误提示友好的错误信息和重试机制
键盘支持支持键盘快捷键操作
移动适配触摸友好的交互设计
无障碍添加ARIA标签

安全考虑

安全项说明
Referer白名单生产环境设置正确的白名单
SN签名敏感操作使用签名验证
密钥保护不在客户端暴露密钥
定期更换定期更新密钥
HTTPS生产环境使用HTTPS

下一步