# baidumap-view
# 百度地图 UTS 插件

> 基于百度地图 SDK 的 `uni-app x` 原生地图插件，支持 Android、iOS、HarmonyOS 三端统一 API，覆盖地图展示、状态控制、覆盖物、富文本气泡与地图事件。

## 支持平台

| 平台 | 支持状态 |
| --- | --- |
| Android | ✅ |
| iOS | ✅ |
| HarmonyOS | ✅ |

## 插件能力

- 地图展示：中心点、缩放、旋转、俯视角、最小/最大缩放级别
- 手势控制：缩放、拖动、旋转、俯视手势开关
- 图层控制：POI、室内图、室内 POI、路况、比例尺、指南针、缩放控件
- 地图类型：标准地图、卫星地图、坐标类型切换、个性化样式
- 视野控制：fitBounds、地图状态获取、可视区域获取、屏幕坐标与经纬度互转
- 地理工具：坐标转换、距离计算、点在多边形/矩形判断
- 覆盖物：`Marker`、`Polyline`、`Polygon`、`Circle`、`Label`
- 富文本气泡：`Marker` 和 `Label` 支持 `RichView`
- POI 检索：城市内检索、周边检索、地理编码、逆地理编码、SUG 联想
- 路线规划：步行、骑行、驾车路线规划
- 地图事件：点击、双击、长按、POI 点击、覆盖物点击、地图状态变化、地图加载/渲染完成
- 调试能力：插件内部日志开关

## 版本要求

- `HBuilderX`：`^4.85`
- `uni-app-x`：`^4.85`

## 快速开始

### 1. 引入插件

```uts
import {
  SdkInit,
  LogUtil,
  createNativeMapContext,
  INativeMapContext,
  LatLng,
  MapOverlay,
  MapPoi,
  Marker,
  Polyline,
  Polygon,
  Circle,
  Label,
  RichView,
  RichViewItem
} from '@/uni_modules/baidumap-view'
```

### 2. 初始化隐私与 AK

使用地图前必须先同意隐私政策并配置百度地图 AK。

```uts
SdkInit.setAgreePrivacy(true)
SdkInit.setAK('你的百度地图AK')
```

### 3. 页面中放置地图组件

`id` 必填，且同一页面内必须唯一。

```html
<baidumap-view
  id="demoMap"
  style="width: 750rpx; height: 900rpx"
  :lat="39.914779"
  :lng="116.404"
  :zoom="13"
  @onMapLoadFinish="onMapLoadFinish"
  @onMapClick="onMapClick"
  @onMapOverlayClick="onMapOverlayClick"
/>
```

### 4. 地图加载完成后创建上下文

```uts
let map : INativeMapContext | null = null

function onMapLoadFinish() {
  map = createNativeMapContext('demoMap', getCurrentInstance()?.proxy ?? null)
}

onBeforeUnmount(() => {
  map?.destroy()
})
```

## 完整页面示例

```html
<template>
  <view>
    <button @click="addMarker">添加标记</button>
    <button @click="moveCenter">移动中心点</button>
    <baidumap-view
      id="demoMap"
      style="width: 750rpx; height: 900rpx"
      :lat="39.914779"
      :lng="116.404"
      :zoom="13"
      :showTrafficMap="true"
      @onMapLoadFinish="onMapLoadFinish"
      @onMapClick="onMapClick"
      @onMapPoiClick="onMapPoiClick"
      @onMapOverlayClick="onMapOverlayClick"
    />
  </view>
</template>

<script setup lang="uts">
import {
  SdkInit,
  createNativeMapContext,
  INativeMapContext,
  LatLng,
  MapOverlay,
  MapPoi,
  Marker,
  LogUtil
} from '@/uni_modules/baidumap-view'

SdkInit.setAgreePrivacy(true)
SdkInit.setAK('你的百度地图AK')
LogUtil.setMapLogStatus(true)

let map : INativeMapContext | null = null
let marker : Marker | null = null

function onMapLoadFinish() {
  map = createNativeMapContext('demoMap', getCurrentInstance()?.proxy ?? null)
}

function addMarker() {
  if (map == null) {
    return
  }
  marker?.remove()

  const newMarker = new Marker()
  const position : LatLng = { lat: 39.92235, lng: 116.380338 }
  newMarker.setPosition(position)
  newMarker.icon = '/static/icon_marker.png'
  newMarker.addToMap('demoMap')
  marker = newMarker
}

function moveCenter() {
  const center : LatLng = { lat: 39.915, lng: 116.404 }
  map?.setCenter(center.lat, center.lng)
  map?.setZoom(15)
}

function onMapClick(position : LatLng) {
  console.log('map click', position)
}

function onMapPoiClick(poi : MapPoi) {
  console.log('poi click', poi.name, poi.uid)
}

function onMapOverlayClick(overlay : MapOverlay) {
  const currentMarker = marker
  if (currentMarker != null && currentMarker.getId() == overlay.id) {
    currentMarker.updateIcon('/static/icon_marker_selected.png')
  }
}

onBeforeUnmount(() => {
  marker?.remove()
  map?.destroy()
})
</script>
```

## 组件属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 必填 | 地图唯一标识 |
| `lat` | `number` | `39.914779` | 初始中心纬度 |
| `lng` | `number` | `116.404000` | 初始中心经度 |
| `zoom` | `number` | `18` | 初始缩放级别 |
| `minZoom` | `number` | `3` | 最小缩放级别 |
| `maxZoom` | `number` | `23` | 最大缩放级别 |
| `rotate` | `number` | `0` | 地图旋转角度 |
| `overlook` | `number` | `0` | 地图俯视角 |
| `showMapPoi` | `boolean` | `true` | 是否展示 POI |
| `showMapIndoorPoi` | `boolean` | `true` | 是否展示室内 POI |
| `showBaseIndoorMap` | `boolean` | `true` | 是否展示室内图 |
| `showTrafficMap` | `boolean` | `true` | 是否展示路况图层 |
| `zoomGesturesEnabled` | `boolean` | `true` | 是否启用缩放手势 |
| `moveGesturesEnabled` | `boolean` | `true` | 是否启用拖动手势 |
| `rotateGesturesEnabled` | `boolean` | `true` | 是否启用旋转手势 |
| `overlookingGesturesEnabled` | `boolean` | `true` | 是否启用俯视手势 |

## 组件事件

| 事件名 | 参数 | 说明 |
| --- | --- | --- |
| `@onMapLoadFinish` | - | 地图加载完成 |
| `@onMapRenderFinish` | - | 地图渲染完成 |
| `@onMapClick` | `LatLng` | 点击地图空白区域 |
| `@onMapDoubleClick` | `LatLng` | 双击地图 |
| `@onMapLongPress` | `LatLng` | 长按地图 |
| `@onMapPoiClick` | `MapPoi` | 点击地图 POI |
| `@onMapOverlayClick` | `MapOverlay` | 点击覆盖物或富文本子项 |
| `@onMapStatusChangeStart` | `number` | 地图状态开始变化 |
| `@onMapStatusChange` | - | 地图状态变化中 |
| `@onMapStatusChangeFinish` | - | 地图状态变化结束 |

### 事件数据结构

```uts
type LatLng = {
  lat : number,
  lng : number
}

type MapPoi = {
  name : string,
  uid : string,
  position : LatLng
}

type MapOverlay = {
  id : number,
  type : 'marker' | 'polyline' | 'polygon' | 'circle' | 'label' | 'multipoint',
  isItemView ?: boolean,
  itemIndex ?: number
}
```

## 地图控制 API

通过 `createNativeMapContext` 获取 `INativeMapContext` 后可调用以下能力：

### 地图状态

```uts
map?.setZoom(15)
map?.setMinZoom(5)
map?.setMaxZoom(20)
map?.setRotate(30)
map?.setOverlook(20)
map?.setCenter(39.915, 116.404)

map?.setMapStatus({
  center: { lat: 39.915, lng: 116.404 },
  zoom: 14,
  rotate: 0,
  overlook: 0,
  minZoom: 3,
  maxZoom: 23,
  isAnimate: true,
  animationTime: 300
})
```

### 手势开关

```uts
map?.zoomGesturesEnabled(true)
map?.moveGesturesEnabled(true)
map?.rotateGesturesEnabled(true)
map?.overlookingGesturesEnabled(true)
```

### 图层与控件

```uts
map?.showMapPoi(true)
map?.showMapIndoorPoi(true)
map?.showBaseIndoorMap(true)
map?.showTrafficMap(true)
map?.showScaleControl(true)
map?.showZoomControls(true)
map?.showCompass(true)
```

### 生命周期

```uts
if (map?.isValid() == true) {
  console.log('map context valid')
}
map?.destroy()
```

### 地图类型与坐标系

```uts
// 地图类型：1-标准地图 2-卫星地图
map?.setMapType(1)

// 坐标类型：1-BD09LL(百度) 2-GCJ02(国测局)
map?.setCoordType(1)

// 地图样式：0-标准 1-深色
map?.setMapStyleMode(0)

// 个性化样式（本地文件）
map?.setCustomMapStylePath('/static/custom_style.sty')

// 个性化样式（样式ID，从百度地图开放平台获取）
map?.setCustomMapStyleById('your_style_id')
```

### 视野控制

```uts
// 调整视野以包含指定坐标点
const bounds : LatLng[] = [
  { lat: 39.915, lng: 116.404 },
  { lat: 39.925, lng: 116.414 }
]
const fitOptions : FitBoundsOptions = {
  padding: 50,
  isAnimate: true,
  animationTime: 300
}
map?.fitBounds(bounds, fitOptions)

// 获取当前地图状态
map?.getMapStatus((status : MapStatus) => {
  console.log('center:', status.center, 'zoom:', status.zoom)
})

// 获取可视区域
map?.getVisibleRegion((region : VisibleRegion) => {
  console.log('northeast:', region.northeast, 'southwest:', region.southwest)
})

// 屏幕坐标与经纬度互转
map?.screenToLatLng(200, 300, (latlng : LatLng) => {
  console.log('screen to latlng:', latlng)
})
map?.latLngToScreen({ lat: 39.915, lng: 116.404 }, (point : ScreenPoint) => {
  console.log('latlng to screen:', point.x, point.y)
})
```

## 覆盖物使用

> 建议所有覆盖物变量声明为 `let xxx : Type | null = null`，并在重复添加前先调用 `remove()`，避免地图上累积旧对象。

### Marker

```uts
let marker : Marker | null = null

function addMarker() {
  marker?.remove()
  const newMarker = new Marker()
  const position : LatLng = { lat: 39.92235, lng: 116.380338 }
  newMarker.setPosition(position)
  newMarker.icon = '/static/icon_marker.png'
  newMarker.addToMap('demoMap')
  marker = newMarker
}

marker?.updatePosition({ lat: 39.915, lng: 116.404 })
marker?.updateIcon('/static/icon_marker_selected.png')
marker?.remove()
```

### Polyline

```uts
let polyline : Polyline | null = null

function addPolyline() {
  polyline?.remove()
  const newPolyline = new Polyline()
  newPolyline.setPoints([
    { lat: 39.915, lng: 116.404 },
    { lat: 39.925, lng: 116.414 },
    { lat: 39.935, lng: 116.424 }
  ])
  newPolyline.color = '#1E80FF'
  newPolyline.width = 8
  newPolyline.addToMap('demoMap')
  polyline = newPolyline
}

polyline?.updatePoints([
  { lat: 39.915, lng: 116.404 },
  { lat: 39.945, lng: 116.434 }
])
polyline?.updateColor('#FF5A5F')
polyline?.updateWidth(10)
```

### Polygon

```uts
let polygon : Polygon | null = null

function addPolygon() {
  polygon?.remove()
  const newPolygon = new Polygon()
  newPolygon.setPointsData([
    { lat: 39.915, lng: 116.404 },
    { lat: 39.925, lng: 116.434 },
    { lat: 39.905, lng: 116.444 }
  ])
  newPolygon.borderWidth = 4
  newPolygon.borderColor = '#1E80FF'
  newPolygon.fillColor = '#4D1E80FF'
  newPolygon.addToMap('demoMap')
  polygon = newPolygon
}

polygon?.updatePoints([
  { lat: 39.915, lng: 116.404 },
  { lat: 39.935, lng: 116.424 },
  { lat: 39.905, lng: 116.444 }
])
polygon?.updateBorderWidth(6)
polygon?.updateBorderColor('#FF5A5F')
polygon?.updateFillColor('#4DFF5A5F')
```

### Circle

```uts
let circle : Circle | null = null

function addCircle() {
  circle?.remove()
  const newCircle = new Circle()
  newCircle.setCenter({ lat: 39.915, lng: 116.404 })
  newCircle.radius = 500
  newCircle.borderWidth = 4
  newCircle.borderColor = '#1E80FF'
  newCircle.fillColor = '#4D1E80FF'
  newCircle.addToMap('demoMap')
  circle = newCircle
}

circle?.updateCenter({ lat: 39.918, lng: 116.408 })
circle?.updateRadius(800)
circle?.updateBorderWidth(6)
circle?.updateBorderColor('#FF5A5F')
circle?.updateFillColor('#4DFF5A5F')
```

### Label

```uts
let label : Label | null = null

function addLabel() {
  label?.remove()
  const newLabel = new Label()
  newLabel.setPosition({ lat: 39.915, lng: 116.404 })
  newLabel.text = '百度地图'
  newLabel.fontSize = 28
  newLabel.fontColor = '#1F2329'
  newLabel.borderWidth = 2
  newLabel.borderColor = '#FFFFFF'
  newLabel.rotate = 0
  newLabel.addToMap('demoMap')
  label = newLabel
}

label?.updateText('地图插件')
label?.updateFontSize(32)
label?.updateFontColor('#1E80FF')
label?.updateBorderWidth(3)
label?.updateBorderColor('#FF5A5F')
label?.updateRotate(10)
label?.updatePosition({ lat: 39.918, lng: 116.408 })
```

### RichView 富文本气泡

`Marker` 和 `Label` 支持 `addRichView()` / `removeRichView()`，可用于实现信息气泡、组合图文标注等场景。

#### RichView 类型

```uts
type RichViewItem = {
  type : string,
  text ?: string,
  fontSize ?: number,
  fontColor ?: string,
  fontBold ?: boolean,
  borderWidth ?: number,
  borderColor ?: string,
  maxLines ?: number,
  image ?: string,
  direction ?: 'vertical' | 'horizontal',
  children ?: Array<RichViewItem>,
  width ?: number,
  height ?: number,
  background ?: string,
  backgroundColor ?: string,
  margin ?: {
    top ?: number,
    left ?: number,
    bottom ?: number,
    right ?: number
  },
  gravity ?: 'left' | 'center' | 'right',
  clickable ?: boolean
}

type RichView = {
  items : Array<RichViewItem>,
  direction ?: 'vertical' | 'horizontal',
  locate ?: 'top' | 'bottom' | 'left' | 'right',
  offsetX ?: number,
  offsetY ?: number,
  gravity ?: 'left' | 'center' | 'right',
  background ?: string,
  backgroundColor ?: string,
  collisionPriority ?: number
}
```

#### 给 Marker 添加气泡

```uts
const richView : RichView = {
  locate: 'top',
  offsetY: -12,
  direction: 'vertical',
  items: [
    {
      type: 'layout',
      direction: 'horizontal',
      children: [
        {
          type: 'image',
          image: '/static/avatar.png',
          width: 48,
          height: 48,
          margin: { right: 8 }
        },
        {
          type: 'text',
          text: '教学点位',
          fontSize: 26,
          fontColor: '#1F2329',
          fontBold: true,
          clickable: true
        }
      ]
    }
  ]
}

marker?.addRichView(richView)
marker?.removeRichView()
```

点击 RichView 子项时，`@onMapOverlayClick` 的 `MapOverlay` 会携带：

- `isItemView`
- `itemIndex`

可据此判断是否点击了气泡内某个元素。

## 检索服务

通过 `map?.getSearchService()` 获取检索服务实例。

### POI 检索

```uts
const searchService = map?.getSearchService()

// 城市内检索
searchService?.poiCitySearch({
  keyword: '餐厅',
  city: '北京',
  pageIndex: 0,
  pageSize: 10
}, (result) => {
  result.poiList?.forEach((poi) => {
    console.log(poi.name, poi.address, poi.location)
  })
})

// 周边检索
searchService?.poiNearbySearch({
  keyword: '酒店',
  location: { lat: 39.915, lng: 116.404 },
  radius: 1000,
  pageIndex: 0,
  pageSize: 10
}, (result) => {
  result.poiList?.forEach((poi) => {
    console.log(poi.name, poi.distance)
  })
})
```

### 地理编码

```uts
// 地址 -> 坐标
searchService?.geoCode({
  address: '北京市海淀区上地十街10号',
  city: '北京'
}, (result) => {
  console.log('location:', result.location)
})

// 坐标 -> 地址
searchService?.reverseGeoCode({
  location: { lat: 39.915, lng: 116.404 }
}, (result) => {
  console.log('address:', result.address)
  console.log('poiList:', result.poiList)
})
```

### SUG 联想检索

```uts
searchService?.suggestionSearch({
  keyword: '天安门',
  city: '北京'
}, (result) => {
  result.suggestionList?.forEach((item) => {
    console.log(item.key, item.address, item.location)
  })
})
```

## 路线规划

### 步行路线

```uts
searchService?.walkingRouteSearch({
  startLocation: { lat: 39.915, lng: 116.404 },
  endLocation: { lat: 39.925, lng: 116.414 }
}, (result) => {
  const route = result.routeLines?.[0]
  if (route != null) {
    console.log('distance:', route.distance, 'duration:', route.duration)
    // route.steps 包含路线各步骤
    // route.wayPoints 包含路线点串，可用于绑定 Polyline
  }
})
```

### 骑行路线

```uts
searchService?.ridingRouteSearch({
  startLocation: { lat: 39.915, lng: 116.404 },
  endLocation: { lat: 39.925, lng: 116.414 }
}, (result) => {
  const route = result.routeLines?.[0]
  if (route != null) {
    console.log('distance:', route.distance, 'duration:', route.duration)
  }
})
```

### 驾车路线

```uts
searchService?.drivingRouteSearch({
  startLocation: { lat: 39.915, lng: 116.404 },
  endLocation: { lat: 39.935, lng: 116.434 }
}, (result) => {
  const route = result.routeLines?.[0]
  if (route != null) {
    console.log('distance:', route.distance, 'duration:', route.duration)
    // route.trafficInfo 包含路况信息
  }
})
```

### 路线绘制示例

```uts
let routePolyline : Polyline | null = null

function drawRoute(wayPoints : LatLng[]) {
  routePolyline?.remove()
  const polyline = new Polyline()
  polyline.setPoints(wayPoints)
  polyline.color = '#1E80FF'
  polyline.width = 8
  polyline.addToMap('demoMap')
  routePolyline = polyline

  // 调整视野以显示完整路线
  map?.fitBounds(wayPoints, { padding: 80, isAnimate: true })
}
```

## 地理工具

```uts
const geoUtils = map?.getGeoUtils()

// 坐标转换
const gcj02 = geoUtils?.bd09llToGcj02({ lat: 39.915, lng: 116.404 })
const bd09ll = geoUtils?.gcj02ToBd09ll({ lat: 39.91, lng: 116.40 })

// 计算两点距离（米）
const distance = geoUtils?.getDistance(
  { lat: 39.915, lng: 116.404 },
  { lat: 39.925, lng: 116.414 }
)

// 判断点是否在多边形内
const polygon : LatLng[] = [
  { lat: 39.91, lng: 116.40 },
  { lat: 39.92, lng: 116.41 },
  { lat: 39.91, lng: 116.42 }
]
const inPolygon = geoUtils?.isPointInPolygon({ lat: 39.915, lng: 116.405 }, polygon)

// 判断点是否在矩形范围内
const inBounds = geoUtils?.isPointInBounds(
  { lat: 39.915, lng: 116.405 },
  { lat: 39.91, lng: 116.40 },  // southwest
  { lat: 39.92, lng: 116.42 }   // northeast
)
```

## 推荐使用规范

### 1. 使用局部常量创建覆盖物

为避免 UTS 对可变变量的 Smart Cast 限制，推荐使用局部常量模式：

```uts
let marker : Marker | null = null

function addMarker() {
  marker?.remove()
  const newMarker = new Marker()
  newMarker.setPosition({ lat: 39.92235, lng: 116.380338 })
  newMarker.icon = '/static/icon_marker.png'
  newMarker.addToMap('demoMap')
  marker = newMarker
}
```

### 2. 覆盖物点击时先取局部引用

```uts
function onMapOverlayClick(overlay : MapOverlay) {
  const currentMarker = marker
  if (currentMarker != null && currentMarker.getId() == overlay.id) {
    currentMarker.updateIcon('/static/icon_marker_selected.png')
  }
}
```

### 3. 注意资源路径

- 图片资源建议使用项目内绝对静态路径，如 `'/static/icon_marker.png'`
- `Marker.icon`、`RichView.image`、`RichView.background` 会在原生层转换为绝对路径

### 4. 页面卸载时释放资源

```uts
onBeforeUnmount(() => {
  marker?.remove()
  map?.destroy()
})
```

## 调试日志

```uts
LogUtil.setMapLogStatus(true)
console.log(LogUtil.getMapLogStatus())
```

## 常见问题

### 1. 地图不显示

请检查：

- 是否已调用 `SdkInit.setAgreePrivacy(true)`
- 是否已正确设置 `SdkInit.setAK(...)`
- 地图组件是否设置了明确宽高
- `id` 是否唯一

### 2. `createNativeMapContext` 返回空

请检查：

- 是否在 `@onMapLoadFinish` 之后再创建上下文
- `createNativeMapContext('地图id', getCurrentInstance()?.proxy ?? null)` 中的 `id` 是否与组件一致

### 3. 覆盖物添加失败

请检查：

- 是否先创建了地图上下文
- `LatLng` 坐标是否合法
- `Marker.icon` 或 RichView 图片资源路径是否有效
- `addToMap('xxx')` 的参数是否与地图 `id` 一致

## 官方文档

- [百度地图 SDK Android 文档](https://lbs.baidu.com/docs/android?title=androidsdk/index)
- [百度地图 SDK iOS 文档](https://lbs.baidu.com/docs/ios?title=iossdk/index)
- [百度地图 SDK Harmony 文档](https://lbs.baidu.com/docs/harmony?title=harmonynextsdk/index)