HarmonyOS 鸿蒙Next中使用 Site Kit 实现 POI 搜索功能

HarmonyOS 鸿蒙Next中使用 Site Kit 实现 POI 搜索功能 在开发地图类应用时,需要实现 POI(兴趣点)搜索功能:

  1. 用户输入关键词(如"咖啡"、“酒店”),搜索周边相关地点
  2. 返回地点名称、地址、距离、评分等详细信息
  3. 支持按距离排序,限定搜索半径
  4. 获取地点的营业时间、联系电话等信息
3 回复

1. 配置 Site Kit 依赖

oh-package.json5 中确认已引入 MapKit:

{
  "dependencies": {
    "@kit.MapKit": "^5.0.0"
  }
}

2. 定义 POI 数据接口

// services/HuaweiPoiSearchService.ets
import { site } from '@kit.MapKit'
import { BusinessError } from '@kit.BasicServicesKit'

/**
 * POI 位置接口
 */
export interface HuaweiPoiLocation {
  latitude: number
  longitude: number
}

/**
 * POI 营业时间接口
 */
export interface HuaweiPoiOpeningHours {
  periods?: Array<{
    open?: { day?: number, time?: string }
    close?: { day?: number, time?: string }
  }>
  openNow?: boolean
}

/**
 * POI 项目接口
 */
export interface HuaweiPoiItem {
  // 基础信息
  poiId: string
  title: string
  snippet: string        // 地址
  typeDes: string        // 类型描述
  
  // 位置信息
  latitude: number
  longitude: number
  distance: number       // 距离(米)
  
  // 联系信息
  tel?: string           // 电话
  website?: string       // 网站
  
  // 评价信息
  rating?: string        // 评分
  starRating?: string    // 星级
  
  // 营业信息
  openingHours?: HuaweiPoiOpeningHours
  businessStatus?: string
  
  // 地址信息
  formatAddress?: string
  
  // 分类信息
  poiTypes?: string[]
}

/**
 * POI 搜索结果接口
 */
export interface HuaweiPoiSearchResult {
  pois: HuaweiPoiItem[]
  totalCount: number
  pageNum: number
  pageSize: number
}

/**
 * POI 搜索参数接口
 */
export interface HuaweiPoiSearchParams {
  keyword: string        // 搜索关键词
  latitude: number       // 中心点纬度
  longitude: number      // 中心点经度
  radius: number         // 搜索半径(米)
  pageNum?: number       // 页码(默认1)
  pageSize?: number      // 每页数量(默认20)
}

3. 实现 POI 搜索服务

/**
 * Site Kit POI 搜索服务类
 */
export class HuaweiPoiSearchService {
  
  // 默认配置
  private static readonly DEFAULT_PAGE_SIZE: number = 20
  private static readonly DEFAULT_LANGUAGE: string = 'zh'
  private static readonly MAX_RADIUS: number = 50000  // 最大半径 50km

  /**
   * 周边 POI 搜索
   * @param params 搜索参数
   * @returns Promise<HuaweiPoiSearchResult>
   */
  static async searchNearby(params: HuaweiPoiSearchParams): Promise<HuaweiPoiSearchResult> {
    // 参数验证
    if (!params.keyword || params.keyword.trim() === '') {
      throw new Error('搜索关键词不能为空')
    }
    if (params.radius <= 0 || params.radius > this.MAX_RADIUS) {
      throw new Error(`搜索半径必须在 1-${this.MAX_RADIUS} 米之间`)
    }
    
    try {
      console.info('[PoiSearch] 开始周边搜索')
      console.info(`  关键词: ${params.keyword}`)
      console.info(`  中心点: ${params.latitude}, ${params.longitude}`)
      console.info(`  半径: ${params.radius}米`)
      
      // 构建 Site Kit 搜索参数
      const searchParams: site.SearchByTextParams = {
        query: params.keyword.trim(),
        location: {
          latitude: params.latitude,
          longitude: params.longitude
        },
        radius: params.radius,
        language: this.DEFAULT_LANGUAGE,
        pageIndex: params.pageNum || 1,
        pageSize: params.pageSize || this.DEFAULT_PAGE_SIZE
      }
      
      // 调用 Site Kit API
      const siteResult = await site.searchByText(searchParams)
      
      if (!siteResult) {
        throw new Error('Site Kit API 返回结果为空')
      }
      
      console.info(`[PoiSearch] ✅ 搜索成功,返回 ${siteResult.sites?.length || 0} 个结果`)
      
      // 转换为统一格式
      const pois = this.parseSiteKitData(siteResult, params.latitude, params.longitude)
      
      return {
        pois: pois,
        totalCount: siteResult.totalCount || pois.length,
        pageNum: params.pageNum || 1,
        pageSize: params.pageSize || this.DEFAULT_PAGE_SIZE
      }
      
    } catch (err) {
      const error = err as BusinessError
      console.error(`[PoiSearch] ❌ 搜索失败: ${error.code} - ${error.message}`)
      throw new Error(`POI 搜索失败: ${error.message}`)
    }
  }
}

4. 解析 Site Kit 返回数据

/**
 * 解析 Site Kit 返回的 POI 数据
 */
private static parseSiteKitData(
  siteResult: site.SearchByTextResult,
  centerLat: number,
  centerLng: number
): HuaweiPoiItem[] {
  if (!siteResult.sites || siteResult.sites.length === 0) {
    console.info('[PoiSearch] 未找到 POI 数据')
    return []
  }
  
  const pois: HuaweiPoiItem[] = []
  
  for (let i = 0; i < siteResult.sites.length; i++) {
    const siteInfo = siteResult.sites[i]
    
    try {
      // 检查位置信息
      if (!siteInfo.location) {
        console.warn(`[PoiSearch] POI ${i} 缺少位置信息`)
        continue
      }
      
      // 计算距离
      const distance = this.calculateDistance(
        centerLat, centerLng,
        siteInfo.location.latitude,
        siteInfo.location.longitude
      )
      
      const poi: HuaweiPoiItem = {
        // 基础信息
        poiId: siteInfo.siteId || `poi_${Date.now()}_${i}`,
        title: siteInfo.name || '未知地点',
        snippet: this.parseAddress(siteInfo),
        typeDes: this.parseSiteType(siteInfo),
        
        // 位置信息
        latitude: siteInfo.location.latitude,
        longitude: siteInfo.location.longitude,
        distance: distance,
        
        // 联系信息
        tel: siteInfo.poi?.phone,
        
        // 评价信息
        rating: this.parseRating(siteInfo.poi),
        starRating: this.parseStarRating(siteInfo.poi),
        
        // 营业信息
        openingHours: this.parseOpeningHours(siteInfo.poi),
        
        // 地址信息
        formatAddress: siteInfo.formatAddress,
        
        // 分类信息
        poiTypes: siteInfo.poi?.poiTypes
      }
      
      pois.push(poi)
    } catch (err) {
      console.error('[PoiSearch] 解析 POI 数据失败:', err)
    }
  }
  
  console.info(`[PoiSearch] 解析完成,共 ${pois.length} 个 POI`)
  return pois
}

/**
 * 解析地址
 */
private static parseAddress(siteInfo: site.Site): string {
  if (siteInfo.formatAddress) {
    return siteInfo.formatAddress
  }
  
  // 组合地址字段
  const parts: string[] = []
  if (siteInfo.address?.adminArea) parts.push(siteInfo.address.adminArea)
  if (siteInfo.address?.locality) parts.push(siteInfo.address.locality)
  if (siteInfo.address?.subLocality) parts.push(siteInfo.address.subLocality)
  if (siteInfo.address?.thoroughfare) parts.push(siteInfo.address.thoroughfare)
  
  return parts.join('') || '地址未知'
}

/**
 * 解析 POI 类型
 */
private static parseSiteType(siteInfo: site.Site): string {
  if (siteInfo.poi?.poiTypes && siteInfo.poi.poiTypes.length > 0) {
    return siteInfo.poi.poiTypes[0]
  }
  return '其他'
}

/**
 * 解析评分
 */
private static parseRating(poiInfo: site.Poi | undefined): string | undefined {
  if (!poiInfo || !poiInfo.rating) return undefined
  return poiInfo.rating.toFixed(1)
}

/**
 * 解析星级评分
 */
private static parseStarRating(poiInfo: site.Poi | undefined): string | undefined {
  if (!poiInfo || !poiInfo.starRating) return undefined
  return poiInfo.starRating.toFixed(1)
}

/**
 * 解析营业时间
 */
private static parseOpeningHours(poiInfo: site.Poi | undefined): HuaweiPoiOpeningHours | undefined {
  if (!poiInfo || !poiInfo.openingHours) return undefined
  
  return {
    periods: poiInfo.openingHours.periods?.map(period => ({
      open: period.open ? { day: period.open.week, time: period.open.time } : undefined,
      close: period.close ? { day: period.close.week, time: period.close.time } : undefined
    }))
  }
}

5. 计算两点间距离

/**
 * 计算两点间距离(Haversine 公式)
 * @returns 距离(米)
 */
private static calculateDistance(
  lat1: number, lng1: number,
  lat2: number, lng2: number
): number {
  const R = 6371000  // 地球半径(米)
  const dLat = this.toRadians(lat2 - lat1)
  const dLng = this.toRadians(lng2 - lng1)
  
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
    Math.sin(dLng / 2) * Math.sin(dLng / 2)
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
  
  return Math.round(R * c)
}

private static toRadians(degrees: number): number {
  return degrees * Math.PI / 180
}

6. 在页面中使用

// pages/PoiSearchPage.ets
import { HuaweiPoiSearchService, HuaweiPoiItem } from '../services/HuaweiPoiSearchService'

@Entry
@Component
struct PoiSearchPage {
  @State keyword: string = ''
  @State searchResults: HuaweiPoiItem[] = []
  @State isSearching: boolean = false
  @State errorMessage: string = ''
  
  // 当前位置(示例:北京天安门)
  private currentLat: number = 39.9042
  private currentLng: number = 116.4074
  
  build() {
    Column({ space: 16 }) {
      // 搜索框
      Row({ space: 8 }) {
        TextInput({ placeholder: '搜索周边...', text: this.keyword })
          .layoutWeight(1)
          .height(44)
          .onChange((value: string) => this.keyword = value)
        
        Button('搜索')
          .width(80)
          .height(44)
          .onClick(() => this.doSearch())
      }
      .width('100%')
      
      // 搜索状态
      if (this.isSearching) {
        Row() {
          LoadingProgress().width(24).height(24)
          Text('搜索中...').margin({ left: 8 })
        }
      }
      
      // 错误提示
      if (this.errorMessage) {
        Text(this.errorMessage)
          .fontColor('#FF4444')
          .fontSize(14)
      }
      
      // 搜索结果列表
      List() {
        ForEach(this.searchResults, (poi: HuaweiPoiItem) => {
          ListItem() {
            Column({ space: 4 }) {
              Row() {
                Text(poi.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .layoutWeight(1)
                Text(`${poi.distance}m`)
                  .fontSize(14)
                  .fontColor('#666666')
              }
              .width('100%')
              
              Text(poi.snippet)
                .fontSize(14)
                .fontColor('#999999')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              
              Row({ space: 16 }) {
                Text(poi.typeDes)
                  .fontSize(12)
                  .fontColor('#A9846A')
                
                if (poi.rating) {
                  Text(`⭐ ${poi.rating}`)
                    .fontSize(12)
                    .fontColor('#FF9500')
                }
                
                if (poi.tel) {
                  Text(`📞 ${poi.tel}`)
                    .fontSize(12)
                    .fontColor('#007AFF')
                }
              }
            }
            .alignItems(HorizontalAlign.Start)
            .padding(12)
          }
        })
      }
      .layoutWeight(1)
      .divider({ strokeWidth: 1, color: '#E8E8E8' })
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
  
  /**
   * 执行搜索
   */
  private async doSearch(): Promise<void> {
    if (!this.keyword.trim()) {
      this.errorMessage = '请输入搜索关键词'
      return
    }
    
    this.isSearching = true
    this.errorMessage = ''
    this.searchResults = []
    
    try {
      const result = await HuaweiPoiSearchService.searchNearby({
        keyword: this.keyword,
        latitude: this.currentLat,
        longitude: this.currentLng,
        radius: 5000,  // 5公里范围
        pageSize: 20
      })
      
      this.searchResults = result.pois
      
      if (result.pois.length === 0) {
        this.errorMessage = '未找到相关地点'
      }
    } catch (error) {
      this.errorMessage = `搜索失败: ${error}`
    } finally {
      this.isSearching = false
    }
  }
}

效果

控制台日志:

[PoiSearch] 开始周边搜索
  关键词: 咖啡
  中心点: 39.9042, 116.4074
  半径: 5000米
[PoiSearch] ✅ 搜索成功,返回 20 个结果
[PoiSearch] 解析完成,共 20 个 POI

搜索结果示例:

名称 距离 类型 评分
星巴克(故宫店) 320m 咖啡厅 4.5
瑞幸咖啡(王府井店) 580m 咖啡厅 4.2
Costa Coffee 890m 咖啡厅 4.3

关键点总结

要点 说明
API 调用 site.searchByText() 关键字搜索
搜索参数 query、location、radius、pageSize
距离计算 Haversine 公式计算球面距离
数据转换 Site 结构 → 统一 POI 接口
错误处理 BusinessError 获取错误码

更多关于HarmonyOS 鸿蒙Next中使用 Site Kit 实现 POI 搜索功能的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,使用Site Kit实现POI搜索功能,首先需在AppGallery Connect配置应用并获取API Key。然后在项目中集成Site Kit SDK,并在module.json5中声明ohos.permission.LOCATION和ohos.permission.APPROXIMATELY_LOCATION权限。

核心步骤是创建SearchService实例,设置SearchOption参数(如keyword、location、radius等),最后调用searchByKeyword方法进行异步搜索并处理返回的PoiData结果集。

在HarmonyOS Next中,使用Site Kit实现POI搜索功能,可以高效地获取地点信息。以下是核心实现步骤:

1. 环境配置与权限申请

  • module.json5中声明ohos.permission.LOCATIONohos.permission.APPROXIMATELY_LOCATION权限。
  • 在项目级build-profile.json5dependencies中添加Site Kit依赖:"@ohos/site": "x.x.x"

2. 初始化与关键词搜索

  • 导入@ohos.site模块。
  • 使用search.search接口,构建SearchRequest对象。关键参数包括:
    • keyword: 用户输入的关键词(如“咖啡”)。
    • location: 通过geoLocationManager获取的当前经纬度坐标,作为搜索中心点。
    • radius (可选): 设置搜索半径(单位:米),例如5000。
    • pageIndexpageSize: 用于分页。
import { search } from '@ohos/site';

let request: search.SearchRequest = {
  keyword: '咖啡',
  location: { lat: 31.2304, lng: 121.4737 }, // 示例坐标,实际应从定位模块获取
  radius: 5000,
  pageIndex: 1,
  pageSize: 20
};
search.search(request).then((data) => {
  // 处理搜索结果
}).catch((err) => {
  // 处理错误
});

3. 处理搜索结果与排序

  • search接口返回的SearchResponse中的sites数组即POI列表。
  • 每个Site对象默认包含:name(名称)、address(地址)、location(经纬度)、distance(距中心点的距离,单位米)等基础信息。
  • 如需按距离排序,可直接对sites数组基于distance字段进行升序排序。

4. 获取地点详情

  • 要获取营业时间、联系电话、评分等详细信息,需调用search.searchDetail接口。
  • 传入参数为SearchDetailRequest,其中siteId为POI的唯一标识(从Site对象中获取)。
let detailRequest: search.SearchDetailRequest = {
  siteId: 'xxx' // 从Site对象中获取
};
search.searchDetail(detailRequest).then((detailData) => {
  // detailData中包含formattedAddress(详细地址)、phone(电话)、rating(评分)、openingHours(营业时间)等字段
}).catch((err) => {
  // 处理错误
});

关键点说明

  • 定位location参数建议使用系统定位能力动态获取,确保搜索围绕用户当前位置进行。
  • 排序search接口返回结果已默认按相关性排序,如需严格按距离排序,需在客户端对sites数组进行二次处理。
  • 详情查询:基础搜索(search)返回的信息字段有限,完整信息(如电话、营业时间)必须通过searchDetail单独查询。
  • 错误处理:务必对网络异常、权限拒绝、服务端错误等进行捕获和处理。

通过以上步骤,即可在应用中实现关键词POI搜索、距离过滤与排序,以及详情信息展示的功能。

回到顶部