实战案例
本文档提供百度地图的典型应用场景示例,帮助开发者快速实现各类地图功能。
案例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 |