HarmonyOS 鸿蒙Next中使用 Site Kit 实现 POI 搜索功能
HarmonyOS 鸿蒙Next中使用 Site Kit 实现 POI 搜索功能 在开发地图类应用时,需要实现 POI(兴趣点)搜索功能:
- 用户输入关键词(如"咖啡"、“酒店”),搜索周边相关地点
- 返回地点名称、地址、距离、评分等详细信息
- 支持按距离排序,限定搜索半径
- 获取地点的营业时间、联系电话等信息
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.LOCATION和ohos.permission.APPROXIMATELY_LOCATION权限。 - 在项目级
build-profile.json5的dependencies中添加Site Kit依赖:"@ohos/site": "x.x.x"。
2. 初始化与关键词搜索
- 导入
@ohos.site模块。 - 使用
search.search接口,构建SearchRequest对象。关键参数包括:keyword: 用户输入的关键词(如“咖啡”)。location: 通过geoLocationManager获取的当前经纬度坐标,作为搜索中心点。radius(可选): 设置搜索半径(单位:米),例如5000。pageIndex和pageSize: 用于分页。
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搜索、距离过滤与排序,以及详情信息展示的功能。

