expo-gaode-map-search 1.1.2-next.1 → 1.1.2-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,354 @@
1
+ package expo.modules.gaodemap.search
2
+
3
+ import android.content.Context
4
+ import com.amap.api.services.core.LatLonPoint
5
+ import com.amap.api.services.core.PoiItem
6
+ import com.amap.api.services.help.Inputtips
7
+ import com.amap.api.services.help.InputtipsQuery
8
+ import com.amap.api.services.help.Tip
9
+ import com.amap.api.services.poisearch.PoiResult
10
+ import com.amap.api.services.poisearch.PoiSearch
11
+ import com.amap.api.services.route.RouteSearch
12
+ import com.amap.api.services.routepoisearch.RoutePOISearch
13
+ import com.amap.api.services.routepoisearch.RoutePOISearchQuery
14
+ import com.amap.api.services.routepoisearch.RoutePOISearchResult
15
+ import expo.modules.kotlin.Promise
16
+ import expo.modules.kotlin.modules.Module
17
+ import expo.modules.kotlin.modules.ModuleDefinition
18
+
19
+ class ExpoGaodeMapSearchModule : Module() {
20
+ private val context: Context
21
+ get() = appContext.reactContext ?: throw Exception("React context is null")
22
+
23
+ override fun definition() = ModuleDefinition {
24
+ Name("ExpoGaodeMapSearch")
25
+
26
+ /**
27
+ * POI 搜索
28
+ */
29
+ AsyncFunction("searchPOI") { options: Map<String, Any?>, promise: Promise ->
30
+ try {
31
+ val keyword = options["keyword"] as? String
32
+ ?: throw Exception("keyword is required")
33
+
34
+ val city = options["city"] as? String ?: ""
35
+ val types = options["types"] as? String ?: ""
36
+ val pageSize = (options["pageSize"] as? Number)?.toInt() ?: 20
37
+ val pageNum = (options["pageNum"] as? Number)?.toInt() ?: 1
38
+
39
+ val query = PoiSearch.Query(keyword, types, city)
40
+ query.pageSize = pageSize
41
+ query.pageNum = pageNum - 1 // 高德 SDK 从 0 开始
42
+
43
+ val poiSearch = PoiSearch(context, query)
44
+
45
+ poiSearch.setOnPoiSearchListener(object : PoiSearch.OnPoiSearchListener {
46
+ override fun onPoiSearched(result: PoiResult?, rCode: Int) {
47
+ if (rCode == 1000) {
48
+ promise.resolve(convertPoiResult(result))
49
+ } else {
50
+ promise.reject("SEARCH_ERROR", "Search failed with code: $rCode", null)
51
+ }
52
+ }
53
+
54
+ override fun onPoiItemSearched(item: PoiItem?, rCode: Int) {
55
+ // 不使用单个 POI 搜索
56
+ }
57
+ })
58
+
59
+ poiSearch.searchPOIAsyn()
60
+ } catch (e: Exception) {
61
+ promise.reject("SEARCH_ERROR", e.message, e)
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 周边搜索
67
+ */
68
+ AsyncFunction("searchNearby") { options: Map<String, Any?>, promise: Promise ->
69
+ try {
70
+ val keyword = options["keyword"] as? String
71
+ ?: throw Exception("keyword is required")
72
+
73
+ val center = options["center"] as? Map<String, Any?>
74
+ ?: throw Exception("center is required")
75
+
76
+ val latitude = (center["latitude"] as? Number)?.toDouble()
77
+ ?: throw Exception("center.latitude is required")
78
+ val longitude = (center["longitude"] as? Number)?.toDouble()
79
+ ?: throw Exception("center.longitude is required")
80
+
81
+ val radius = (options["radius"] as? Number)?.toInt() ?: 1000
82
+ val types = options["types"] as? String ?: ""
83
+ val pageSize = (options["pageSize"] as? Number)?.toInt() ?: 20
84
+ val pageNum = (options["pageNum"] as? Number)?.toInt() ?: 1
85
+
86
+ val query = PoiSearch.Query(keyword, types)
87
+ query.pageSize = pageSize
88
+ query.pageNum = pageNum - 1
89
+
90
+ val poiSearch = PoiSearch(context, query)
91
+ poiSearch.setBound(PoiSearch.SearchBound(
92
+ LatLonPoint(latitude, longitude), radius
93
+ ))
94
+
95
+ poiSearch.setOnPoiSearchListener(object : PoiSearch.OnPoiSearchListener {
96
+ override fun onPoiSearched(result: PoiResult?, rCode: Int) {
97
+ if (rCode == 1000) {
98
+ promise.resolve(convertPoiResult(result))
99
+ } else {
100
+ promise.reject("SEARCH_ERROR", "Search failed with code: $rCode", null)
101
+ }
102
+ }
103
+
104
+ override fun onPoiItemSearched(item: PoiItem?, rCode: Int) {}
105
+ })
106
+
107
+ poiSearch.searchPOIAsyn()
108
+ } catch (e: Exception) {
109
+ promise.reject("SEARCH_ERROR", e.message, e)
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 沿途搜索
115
+ */
116
+ AsyncFunction("searchAlong") { options: Map<String, Any?>, promise: Promise ->
117
+ try {
118
+ val keyword = options["keyword"] as? String
119
+ ?: throw Exception("keyword is required")
120
+
121
+ val polyline = options["polyline"] as? List<Map<String, Any?>>
122
+ ?: throw Exception("polyline is required")
123
+
124
+ if (polyline.size < 2) {
125
+ throw Exception("polyline must have at least 2 points")
126
+ }
127
+
128
+ // 转换路线点
129
+ val points = polyline.map { point ->
130
+ val lat = (point["latitude"] as? Number)?.toDouble()
131
+ ?: throw Exception("Invalid polyline point")
132
+ val lng = (point["longitude"] as? Number)?.toDouble()
133
+ ?: throw Exception("Invalid polyline point")
134
+ LatLonPoint(lat, lng)
135
+ }
136
+
137
+ val startPoint = points.first()
138
+ val endPoint = points.last()
139
+
140
+ // 构造沿途搜索参数
141
+ val searchRange = 250 // 搜索半径(米)
142
+ // 使用枚举类型作为搜索类型
143
+ val searchType = when(keyword.lowercase()) {
144
+ "加油站", "加油" -> RoutePOISearch.RoutePOISearchType.TypeGasStation
145
+ "atm", "银行" -> RoutePOISearch.RoutePOISearchType.TypeATM
146
+ "汽修", "维修" -> RoutePOISearch.RoutePOISearchType.TypeMaintenanceStation
147
+ "厕所", "卫生间" -> RoutePOISearch.RoutePOISearchType.TypeToilet
148
+ else -> RoutePOISearch.RoutePOISearchType.TypeGasStation // 默认搜索加油站
149
+ }
150
+ val query = RoutePOISearchQuery(startPoint, endPoint, 1, searchType, searchRange)
151
+
152
+ val routePOISearch = RoutePOISearch(context, query)
153
+
154
+ routePOISearch.setPoiSearchListener(object : RoutePOISearch.OnRoutePOISearchListener {
155
+ override fun onRoutePoiSearched(result: RoutePOISearchResult?, rCode: Int) {
156
+ if (rCode == 1000 && result != null) {
157
+ promise.resolve(convertRoutePOIResult(result))
158
+ } else {
159
+ promise.reject("SEARCH_ERROR", "Route search failed with code: $rCode", null)
160
+ }
161
+ }
162
+ })
163
+
164
+ routePOISearch.searchRoutePOIAsyn()
165
+ } catch (e: Exception) {
166
+ promise.reject("SEARCH_ERROR", e.message, e)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * 多边形搜索(使用矩形范围代替)
172
+ */
173
+ AsyncFunction("searchPolygon") { options: Map<String, Any?>, promise: Promise ->
174
+ try {
175
+ val keyword = options["keyword"] as? String
176
+ ?: throw Exception("keyword is required")
177
+
178
+ val polygon = options["polygon"] as? List<Map<String, Any?>>
179
+ ?: throw Exception("polygon is required")
180
+
181
+ val types = options["types"] as? String ?: ""
182
+ val pageSize = (options["pageSize"] as? Number)?.toInt() ?: 20
183
+ val pageNum = (options["pageNum"] as? Number)?.toInt() ?: 1
184
+
185
+ // 计算边界矩形
186
+ val points = polygon.map { point ->
187
+ val lat = (point["latitude"] as? Number)?.toDouble()
188
+ ?: throw Exception("Invalid polygon point")
189
+ val lng = (point["longitude"] as? Number)?.toDouble()
190
+ ?: throw Exception("Invalid polygon point")
191
+ LatLonPoint(lat, lng)
192
+ }
193
+
194
+ val minLat = points.minOf { it.latitude }
195
+ val maxLat = points.maxOf { it.latitude }
196
+ val minLng = points.minOf { it.longitude }
197
+ val maxLng = points.maxOf { it.longitude }
198
+
199
+ val query = PoiSearch.Query(keyword, types)
200
+ query.pageSize = pageSize
201
+ query.pageNum = pageNum - 1
202
+
203
+ val poiSearch = PoiSearch(context, query)
204
+ // 使用矩形搜索代替多边形搜索
205
+ poiSearch.setBound(PoiSearch.SearchBound(
206
+ LatLonPoint(minLat, minLng),
207
+ LatLonPoint(maxLat, maxLng)
208
+ ))
209
+
210
+ poiSearch.setOnPoiSearchListener(object : PoiSearch.OnPoiSearchListener {
211
+ override fun onPoiSearched(result: PoiResult?, rCode: Int) {
212
+ if (rCode == 1000) {
213
+ promise.resolve(convertPoiResult(result))
214
+ } else {
215
+ promise.reject("SEARCH_ERROR", "Search failed with code: $rCode", null)
216
+ }
217
+ }
218
+
219
+ override fun onPoiItemSearched(item: PoiItem?, rCode: Int) {}
220
+ })
221
+
222
+ poiSearch.searchPOIAsyn()
223
+ } catch (e: Exception) {
224
+ promise.reject("SEARCH_ERROR", e.message, e)
225
+ }
226
+ }
227
+
228
+ /**
229
+ * 输入提示
230
+ */
231
+ AsyncFunction("getInputTips") { options: Map<String, Any?>, promise: Promise ->
232
+ try {
233
+ val keyword = options["keyword"] as? String
234
+ ?: throw Exception("keyword is required")
235
+
236
+ val city = options["city"] as? String ?: ""
237
+ val types = options["types"] as? String ?: ""
238
+
239
+ val query = InputtipsQuery(keyword, city)
240
+ if (types.isNotEmpty()) {
241
+ query.cityLimit = true
242
+ }
243
+
244
+ val inputtips = Inputtips(context, query)
245
+
246
+ inputtips.setInputtipsListener { tipList, rCode ->
247
+ if (rCode == 1000) {
248
+ promise.resolve(convertTipsResult(tipList))
249
+ } else {
250
+ promise.reject("TIPS_ERROR", "Input tips failed with code: $rCode", null)
251
+ }
252
+ }
253
+
254
+ inputtips.requestInputtipsAsyn()
255
+ } catch (e: Exception) {
256
+ promise.reject("TIPS_ERROR", e.message, e)
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * 转换 POI 搜索结果
263
+ */
264
+ private fun convertPoiResult(result: PoiResult?): Map<String, Any?> {
265
+ if (result == null) {
266
+ return mapOf(
267
+ "pois" to emptyList<Map<String, Any?>>(),
268
+ "total" to 0,
269
+ "pageNum" to 1,
270
+ "pageSize" to 20,
271
+ "pageCount" to 0
272
+ )
273
+ }
274
+
275
+ val pois = result.pois?.map { poi ->
276
+ mapOf(
277
+ "id" to poi.poiId,
278
+ "name" to poi.title,
279
+ "address" to poi.snippet,
280
+ "location" to mapOf(
281
+ "latitude" to poi.latLonPoint?.latitude,
282
+ "longitude" to poi.latLonPoint?.longitude
283
+ ),
284
+ "typeCode" to poi.typeCode,
285
+ "typeDes" to poi.typeDes,
286
+ "tel" to poi.tel,
287
+ "distance" to poi.distance,
288
+ "cityName" to poi.cityName,
289
+ "cityCode" to poi.cityCode,
290
+ "provinceName" to poi.provinceName,
291
+ "adName" to poi.adName,
292
+ "adCode" to poi.adCode
293
+ )
294
+ } ?: emptyList()
295
+
296
+ return mapOf(
297
+ "pois" to pois,
298
+ "total" to result.pageCount * result.query.pageSize,
299
+ "pageNum" to (result.query.pageNum + 1),
300
+ "pageSize" to result.query.pageSize,
301
+ "pageCount" to result.pageCount
302
+ )
303
+ }
304
+
305
+ /**
306
+ * 转换输入提示结果
307
+ */
308
+ private fun convertTipsResult(tips: List<Tip>?): Map<String, Any?> {
309
+ val tipList = tips?.map { tip ->
310
+ mapOf(
311
+ "id" to tip.poiID,
312
+ "name" to tip.name,
313
+ "address" to tip.address,
314
+ "location" to tip.point?.let {
315
+ mapOf(
316
+ "latitude" to it.latitude,
317
+ "longitude" to it.longitude
318
+ )
319
+ },
320
+ "typeCode" to tip.typeCode,
321
+ "cityName" to tip.district,
322
+ "adName" to tip.district
323
+ )
324
+ } ?: emptyList()
325
+
326
+ return mapOf("tips" to tipList)
327
+ }
328
+
329
+ /**
330
+ * 转换沿途 POI 搜索结果
331
+ */
332
+ private fun convertRoutePOIResult(result: RoutePOISearchResult): Map<String, Any?> {
333
+ val pois = result.routePois?.map { poi ->
334
+ mapOf(
335
+ "id" to poi.id,
336
+ "name" to poi.title,
337
+ "address" to "", // RoutePOIItem 没有 address 属性
338
+ "location" to mapOf(
339
+ "latitude" to poi.point?.latitude,
340
+ "longitude" to poi.point?.longitude
341
+ ),
342
+ "distance" to poi.distance
343
+ )
344
+ } ?: emptyList()
345
+
346
+ return mapOf(
347
+ "pois" to pois,
348
+ "total" to pois.size,
349
+ "pageNum" to 1,
350
+ "pageSize" to pois.size,
351
+ "pageCount" to 1
352
+ )
353
+ }
354
+ }
@@ -0,0 +1,415 @@
1
+ import ExpoModulesCore
2
+ import AMapSearchKit
3
+ import AMapFoundationKit
4
+
5
+ public class ExpoGaodeMapSearchModule: Module {
6
+ private var searchAPI: AMapSearchAPI!
7
+ private var searchDelegate: SearchDelegate!
8
+
9
+ public func definition() -> ModuleDefinition {
10
+ Name("ExpoGaodeMapSearch")
11
+
12
+ // 不在 OnCreate 中初始化,改为延迟初始化
13
+
14
+ /**
15
+ * 手动初始化搜索模块(可选)
16
+ * 如果 API Key 已经设置,则直接初始化
17
+ * 如果未设置,会尝试从 Info.plist 读取
18
+ */
19
+ Function("initSearch") {
20
+ self.initSearchAPI()
21
+ }
22
+
23
+ /**
24
+ * POI 搜索
25
+ */
26
+ AsyncFunction("searchPOI") { (options: [String: Any], promise: Promise) in
27
+ // 延迟初始化:在首次使用时才初始化
28
+ self.initSearchAPI()
29
+
30
+ guard let keyword = options["keyword"] as? String else {
31
+ promise.reject("SEARCH_ERROR", "keyword is required")
32
+ return
33
+ }
34
+
35
+ let city = options["city"] as? String ?? ""
36
+ let types = options["types"] as? String ?? ""
37
+ let pageSize = options["pageSize"] as? Int ?? 20
38
+ let pageNum = options["pageNum"] as? Int ?? 1
39
+
40
+ let request = AMapPOIKeywordsSearchRequest()
41
+ request.keywords = keyword
42
+ request.city = city
43
+ request.types = types
44
+ request.page = pageNum
45
+ request.offset = pageSize
46
+
47
+ self.searchDelegate.currentPromise = promise
48
+ self.searchAPI.aMapPOIKeywordsSearch(request)
49
+ }
50
+
51
+ /**
52
+ * 周边搜索
53
+ */
54
+ AsyncFunction("searchNearby") { (options: [String: Any], promise: Promise) in
55
+ self.initSearchAPI()
56
+
57
+ guard let keyword = options["keyword"] as? String else {
58
+ promise.reject("SEARCH_ERROR", "keyword is required")
59
+ return
60
+ }
61
+
62
+ guard let center = options["center"] as? [String: Any],
63
+ let latitude = center["latitude"] as? Double,
64
+ let longitude = center["longitude"] as? Double else {
65
+ promise.reject("SEARCH_ERROR", "center is required")
66
+ return
67
+ }
68
+
69
+ let radius = options["radius"] as? Int ?? 1000
70
+ let types = options["types"] as? String ?? ""
71
+ let pageSize = options["pageSize"] as? Int ?? 20
72
+ let pageNum = options["pageNum"] as? Int ?? 1
73
+
74
+ let request = AMapPOIAroundSearchRequest()
75
+ request.keywords = keyword
76
+ request.location = AMapGeoPoint.location(
77
+ withLatitude: CGFloat(latitude),
78
+ longitude: CGFloat(longitude)
79
+ )
80
+ request.radius = radius
81
+ request.types = types
82
+ request.page = pageNum
83
+ request.offset = pageSize
84
+
85
+ self.searchDelegate.currentPromise = promise
86
+ self.searchAPI.aMapPOIAroundSearch(request)
87
+ }
88
+
89
+ /**
90
+ * 沿途搜索
91
+ * iOS SDK 支持的沿途搜索类型:加油站、ATM、汽修、厕所
92
+ */
93
+ AsyncFunction("searchAlong") { (options: [String: Any], promise: Promise) in
94
+ self.initSearchAPI()
95
+
96
+ guard let keyword = options["keyword"] as? String else {
97
+ promise.reject("SEARCH_ERROR", "keyword is required")
98
+ return
99
+ }
100
+
101
+ guard let polyline = options["polyline"] as? [[String: Any]] else {
102
+ promise.reject("SEARCH_ERROR", "polyline is required")
103
+ return
104
+ }
105
+
106
+ guard polyline.count >= 2 else {
107
+ promise.reject("SEARCH_ERROR", "polyline must have at least 2 points")
108
+ return
109
+ }
110
+
111
+ // 转换路线点
112
+ var points: [AMapGeoPoint] = []
113
+ for point in polyline {
114
+ if let lat = point["latitude"] as? Double,
115
+ let lng = point["longitude"] as? Double {
116
+ points.append(AMapGeoPoint.location(
117
+ withLatitude: CGFloat(lat),
118
+ longitude: CGFloat(lng)
119
+ ))
120
+ }
121
+ }
122
+
123
+ guard let startPoint = points.first,
124
+ let endPoint = points.last else {
125
+ promise.reject("SEARCH_ERROR", "Invalid polyline points")
126
+ return
127
+ }
128
+
129
+ // 根据关键词确定搜索类型
130
+ let lowercaseKeyword = keyword.lowercased()
131
+ var searchType: AMapRoutePOISearchType = .gasStation
132
+ if lowercaseKeyword.contains("加油") || lowercaseKeyword == "加油站" {
133
+ searchType = .gasStation
134
+ } else if lowercaseKeyword.contains("atm") || lowercaseKeyword.contains("银行") {
135
+ searchType = .ATM
136
+ } else if lowercaseKeyword.contains("汽修") || lowercaseKeyword.contains("维修") {
137
+ searchType = .maintenanceStation
138
+ } else if lowercaseKeyword.contains("厕所") || lowercaseKeyword.contains("卫生间") {
139
+ searchType = .toilet
140
+ }
141
+
142
+ let request = AMapRoutePOISearchRequest()
143
+ request.origin = startPoint
144
+ request.destination = endPoint
145
+ request.searchType = searchType
146
+ request.range = 250
147
+ request.strategy = 0
148
+
149
+ self.searchDelegate.currentPromise = promise
150
+ self.searchAPI.aMapRoutePOISearch(request)
151
+ }
152
+
153
+ /**
154
+ * 多边形搜索
155
+ */
156
+ AsyncFunction("searchPolygon") { (options: [String: Any], promise: Promise) in
157
+ self.initSearchAPI()
158
+
159
+ guard let keyword = options["keyword"] as? String else {
160
+ promise.reject("SEARCH_ERROR", "keyword is required")
161
+ return
162
+ }
163
+
164
+ guard let polygon = options["polygon"] as? [[String: Any]] else {
165
+ promise.reject("SEARCH_ERROR", "polygon is required")
166
+ return
167
+ }
168
+
169
+ let types = options["types"] as? String ?? ""
170
+ let pageSize = options["pageSize"] as? Int ?? 20
171
+ let pageNum = options["pageNum"] as? Int ?? 1
172
+
173
+ var points: [AMapGeoPoint] = []
174
+ for point in polygon {
175
+ if let lat = point["latitude"] as? Double,
176
+ let lng = point["longitude"] as? Double {
177
+ points.append(AMapGeoPoint.location(
178
+ withLatitude: CGFloat(lat),
179
+ longitude: CGFloat(lng)
180
+ ))
181
+ }
182
+ }
183
+
184
+ let request = AMapPOIPolygonSearchRequest()
185
+ request.keywords = keyword
186
+ request.polygon = AMapGeoPolygon(points: points)
187
+ request.types = types
188
+ request.page = pageNum
189
+ request.offset = pageSize
190
+
191
+ self.searchDelegate.currentPromise = promise
192
+ self.searchAPI.aMapPOIPolygonSearch(request)
193
+ }
194
+
195
+ /**
196
+ * 输入提示
197
+ */
198
+ AsyncFunction("getInputTips") { (options: [String: Any], promise: Promise) in
199
+ self.initSearchAPI()
200
+
201
+ guard let keyword = options["keyword"] as? String else {
202
+ promise.reject("TIPS_ERROR", "keyword is required")
203
+ return
204
+ }
205
+
206
+ let city = options["city"] as? String ?? ""
207
+ let types = options["types"] as? String ?? ""
208
+
209
+ let request = AMapInputTipsSearchRequest()
210
+ request.keywords = keyword
211
+ request.city = city
212
+ request.types = types
213
+
214
+ self.searchDelegate.currentPromise = promise
215
+ self.searchAPI.aMapInputTipsSearch(request)
216
+ }
217
+ }
218
+
219
+ // MARK: - Private Methods
220
+
221
+ /**
222
+ * 初始化搜索 API(延迟初始化)
223
+ */
224
+ private func initSearchAPI() {
225
+ if searchAPI == nil {
226
+ // 确保 API Key 已设置
227
+ self.ensureAPIKeyIsSet()
228
+
229
+ searchDelegate = SearchDelegate()
230
+ searchAPI = AMapSearchAPI()
231
+ searchAPI.delegate = searchDelegate
232
+
233
+ let apiKey = AMapServices.shared().apiKey
234
+ print("[ExpoGaodeMapSearch] 搜索 API 已初始化,API Key: \(apiKey ?? "未设置")")
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 确保 API Key 已设置
240
+ * 优先级:已设置 > Info.plist > 提示错误
241
+ */
242
+ private func ensureAPIKeyIsSet() {
243
+ // 1. 检查是否已经设置(通过 initSDK 或 AppDelegate)
244
+ if let existingKey = AMapServices.shared().apiKey, !existingKey.isEmpty {
245
+ print("[ExpoGaodeMapSearch] ✓ API Key 已设置")
246
+ return
247
+ }
248
+
249
+ // 2. 尝试从 Info.plist 读取(Config Plugin 会写入)
250
+ if let apiKey = Bundle.main.object(forInfoDictionaryKey: "AMapApiKey") as? String, !apiKey.isEmpty {
251
+ AMapServices.shared().apiKey = apiKey
252
+ AMapServices.shared().enableHTTPS = true
253
+ print("[ExpoGaodeMapSearch] ✓ 从 Info.plist 读取并设置 API Key")
254
+ return
255
+ }
256
+
257
+ // 3. 都没有,提示错误
258
+ print("[ExpoGaodeMapSearch] ✗ 错误: API Key 未设置!")
259
+ print("[ExpoGaodeMapSearch] 请通过以下任一方式设置:")
260
+ print("[ExpoGaodeMapSearch] 1. 在 app.json 的 plugins 中配置 iosApiKey(推荐)")
261
+ print("[ExpoGaodeMapSearch] 2. 调用 ExpoGaodeMap.initSDK({ iosKey: 'your-key' })")
262
+ print("[ExpoGaodeMapSearch] 3. 在 AppDelegate 中调用 [AMapServices sharedServices].apiKey = @\"your-key\"")
263
+ }
264
+ }
265
+
266
+ // MARK: - Search Delegate
267
+ class SearchDelegate: NSObject, AMapSearchDelegate {
268
+ var currentPromise: Promise?
269
+
270
+ /**
271
+ * POI 搜索回调
272
+ */
273
+ func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
274
+ guard let promise = currentPromise else { return }
275
+
276
+ if let response = response {
277
+ promise.resolve(convertPOISearchResponse(response))
278
+ } else {
279
+ promise.reject("SEARCH_ERROR", "Search failed")
280
+ }
281
+
282
+ currentPromise = nil
283
+ }
284
+
285
+ /**
286
+ * 沿途搜索回调
287
+ */
288
+ func onRoutePOISearchDone(_ request: AMapRoutePOISearchRequest!, response: AMapRoutePOISearchResponse!) {
289
+ guard let promise = currentPromise else { return }
290
+
291
+ if let response = response {
292
+ promise.resolve(convertRoutePOISearchResponse(response))
293
+ } else {
294
+ promise.reject("SEARCH_ERROR", "Route search failed")
295
+ }
296
+
297
+ currentPromise = nil
298
+ }
299
+
300
+ /**
301
+ * 输入提示回调
302
+ */
303
+ func onInputTipsSearchDone(_ request: AMapInputTipsSearchRequest!, response: AMapInputTipsSearchResponse!) {
304
+ guard let promise = currentPromise else { return }
305
+
306
+ if let response = response {
307
+ promise.resolve(convertInputTipsResponse(response))
308
+ } else {
309
+ promise.reject("TIPS_ERROR", "Input tips failed")
310
+ }
311
+
312
+ currentPromise = nil
313
+ }
314
+
315
+ /**
316
+ * 搜索失败回调
317
+ */
318
+ func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
319
+ guard let promise = currentPromise else { return }
320
+
321
+ let errorMessage = error?.localizedDescription ?? "Unknown error"
322
+ promise.reject("SEARCH_ERROR", errorMessage)
323
+ currentPromise = nil
324
+ }
325
+
326
+ // MARK: - 转换方法
327
+
328
+ /**
329
+ * 转换 POI 搜索结果
330
+ */
331
+ private func convertPOISearchResponse(_ response: AMapPOISearchResponse) -> [String: Any] {
332
+ let pois = response.pois?.map { poi -> [String: Any] in
333
+ var result: [String: Any] = [
334
+ "id": poi.uid ?? "",
335
+ "name": poi.name ?? "",
336
+ "address": poi.address ?? "",
337
+ "location": [
338
+ "latitude": poi.location?.latitude ?? 0,
339
+ "longitude": poi.location?.longitude ?? 0
340
+ ],
341
+ "typeCode": poi.typecode ?? "",
342
+ "typeDes": poi.type ?? "",
343
+ "tel": poi.tel ?? "",
344
+ "distance": poi.distance,
345
+ "cityName": poi.city ?? "",
346
+ "provinceName": poi.province ?? "",
347
+ "adName": poi.district ?? "",
348
+ "adCode": poi.adcode ?? ""
349
+ ]
350
+ return result
351
+ } ?? []
352
+
353
+ return [
354
+ "pois": pois,
355
+ "total": response.count,
356
+ "pageNum": response.pois?.first?.uid != nil ? 1 : 0,
357
+ "pageSize": response.pois?.count ?? 0,
358
+ "pageCount": (response.count + 19) / 20
359
+ ]
360
+ }
361
+
362
+ /**
363
+ * 转换输入提示结果
364
+ */
365
+ private func convertInputTipsResponse(_ response: AMapInputTipsSearchResponse) -> [String: Any] {
366
+ let tips = response.tips?.map { tip -> [String: Any] in
367
+ var result: [String: Any] = [
368
+ "id": tip.uid ?? "",
369
+ "name": tip.name ?? "",
370
+ "address": tip.address ?? "",
371
+ "typeCode": tip.typecode ?? "",
372
+ "cityName": tip.district ?? "",
373
+ "adName": tip.district ?? ""
374
+ ]
375
+
376
+ if let location = tip.location {
377
+ result["location"] = [
378
+ "latitude": location.latitude,
379
+ "longitude": location.longitude
380
+ ]
381
+ }
382
+
383
+ return result
384
+ } ?? []
385
+
386
+ return ["tips": tips]
387
+ }
388
+
389
+ /**
390
+ * 转换沿途 POI 搜索结果
391
+ */
392
+ private func convertRoutePOISearchResponse(_ response: AMapRoutePOISearchResponse) -> [String: Any] {
393
+ let pois = response.pois?.map { poi -> [String: Any] in
394
+ var result: [String: Any] = [
395
+ "id": poi.uid ?? "",
396
+ "name": poi.name ?? "",
397
+ "address": "",
398
+ "location": [
399
+ "latitude": poi.location?.latitude ?? 0,
400
+ "longitude": poi.location?.longitude ?? 0
401
+ ],
402
+ "distance": poi.distance
403
+ ]
404
+ return result
405
+ } ?? []
406
+
407
+ return [
408
+ "pois": pois,
409
+ "total": pois.count,
410
+ "pageNum": 1,
411
+ "pageSize": pois.count,
412
+ "pageCount": 1
413
+ ]
414
+ }
415
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-gaode-map-search",
3
- "version": "1.1.2-next.1",
3
+ "version": "1.1.2-next.2",
4
4
  "description": "高德地图搜索功能模块 - POI搜索、关键词搜索、周边搜索,需先安装expo-gaode-map",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -21,8 +21,17 @@
21
21
  "author": "尚博信_王强 <wangqiang03@sunboxsoft.com> (https://github.com/TomWq)",
22
22
  "license": "MIT",
23
23
  "homepage": "https://github.com/TomWq/expo-gaode-map#readme",
24
- "dependencies": {
25
- "expo-gaode-map": "^2.2.4-next.0"
24
+ "files": [
25
+ "build/**/*",
26
+ "src/**/*",
27
+ "android/**/*",
28
+ "ios/**/*",
29
+ "expo-module.config.json",
30
+ "README.md",
31
+ "package.json"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
26
35
  },
27
36
  "devDependencies": {
28
37
  "@types/react": "~19.1.0",
@@ -36,6 +45,9 @@
36
45
  "react": "*",
37
46
  "react-native": "*"
38
47
  },
48
+ "dependencies": {
49
+ "expo-gaode-map": "^2.2.4-next.0"
50
+ },
39
51
  "scripts": {
40
52
  "build": "expo-module build",
41
53
  "clean": "expo-module clean",
@@ -0,0 +1,179 @@
1
+ /**
2
+ * 搜索类型
3
+ */
4
+ export enum SearchType {
5
+ /** POI 搜索 */
6
+ POI = 'poi',
7
+ /** 周边搜索 */
8
+ NEARBY = 'nearby',
9
+ /** 沿途搜索 */
10
+ ALONG = 'along',
11
+ /** 多边形搜索 */
12
+ POLYGON = 'polygon',
13
+ /** 输入提示 */
14
+ INPUT_TIPS = 'inputTips',
15
+ }
16
+
17
+ /**
18
+ * 坐标点
19
+ */
20
+ export interface Coordinates {
21
+ latitude: number;
22
+ longitude: number;
23
+ }
24
+
25
+ /**
26
+ * POI 信息
27
+ */
28
+ export interface POI {
29
+ /** POI ID */
30
+ id: string;
31
+ /** 名称 */
32
+ name: string;
33
+ /** 地址 */
34
+ address: string;
35
+ /** 坐标 */
36
+ location: Coordinates;
37
+ /** 类型编码 */
38
+ typeCode: string;
39
+ /** 类型描述 */
40
+ typeDes: string;
41
+ /** 电话 */
42
+ tel?: string;
43
+ /** 距离(米),仅周边搜索返回 */
44
+ distance?: number;
45
+ /** 城市名称 */
46
+ cityName?: string;
47
+ /** 城市编码 */
48
+ cityCode?: string;
49
+ /** 省份名称 */
50
+ provinceName?: string;
51
+ /** 区域名称 */
52
+ adName?: string;
53
+ /** 区域编码 */
54
+ adCode?: string;
55
+ }
56
+
57
+ /**
58
+ * POI 搜索选项
59
+ */
60
+ export interface POISearchOptions {
61
+ /** 搜索关键词 */
62
+ keyword: string;
63
+ /** 城市名称或城市编码(可选) */
64
+ city?: string;
65
+ /** POI 类型(可选),多个类型用 | 分隔 */
66
+ types?: string;
67
+ /** 每页记录数,默认 20,最大 50 */
68
+ pageSize?: number;
69
+ /** 当前页码,从 1 开始,默认 1 */
70
+ pageNum?: number;
71
+ /** 是否按照距离排序,需要设置中心点 */
72
+ sortByDistance?: boolean;
73
+ /** 中心点坐标,用于距离排序或周边搜索 */
74
+ center?: Coordinates;
75
+ }
76
+
77
+ /**
78
+ * 周边搜索选项
79
+ */
80
+ export interface NearbySearchOptions {
81
+ /** 搜索关键词 */
82
+ keyword: string;
83
+ /** 中心点坐标 */
84
+ center: Coordinates;
85
+ /** 搜索半径,单位:米,默认 1000,最大 50000 */
86
+ radius?: number;
87
+ /** POI 类型(可选),多个类型用 | 分隔 */
88
+ types?: string;
89
+ /** 每页记录数,默认 20,最大 50 */
90
+ pageSize?: number;
91
+ /** 当前页码,从 1 开始,默认 1 */
92
+ pageNum?: number;
93
+ }
94
+
95
+ /**
96
+ * 沿途搜索选项
97
+ */
98
+ export interface AlongSearchOptions {
99
+ /** 搜索关键词 */
100
+ keyword: string;
101
+ /** 路线坐标点数组 */
102
+ polyline: Coordinates[];
103
+ /** 搜索范围,单位:米,默认 500,最大 1000 */
104
+ range?: number;
105
+ /** POI 类型(可选),多个类型用 | 分隔 */
106
+ types?: string;
107
+ }
108
+
109
+ /**
110
+ * 多边形搜索选项
111
+ */
112
+ export interface PolygonSearchOptions {
113
+ /** 搜索关键词 */
114
+ keyword: string;
115
+ /** 多边形顶点坐标数组 */
116
+ polygon: Coordinates[];
117
+ /** POI 类型(可选),多个类型用 | 分隔 */
118
+ types?: string;
119
+ /** 每页记录数,默认 20,最大 50 */
120
+ pageSize?: number;
121
+ /** 当前页码,从 1 开始,默认 1 */
122
+ pageNum?: number;
123
+ }
124
+
125
+ /**
126
+ * 输入提示选项
127
+ */
128
+ export interface InputTipsOptions {
129
+ /** 关键词 */
130
+ keyword: string;
131
+ /** 城市名称或城市编码(可选) */
132
+ city?: string;
133
+ /** POI 类型(可选),多个类型用 | 分隔 */
134
+ types?: string;
135
+ }
136
+
137
+ /**
138
+ * 输入提示结果
139
+ */
140
+ export interface InputTip {
141
+ /** 提示 ID */
142
+ id: string;
143
+ /** 名称 */
144
+ name: string;
145
+ /** 地址 */
146
+ address: string;
147
+ /** 坐标(可能为空) */
148
+ location?: Coordinates;
149
+ /** 类型编码 */
150
+ typeCode?: string;
151
+ /** 城市名称 */
152
+ cityName?: string;
153
+ /** 区域名称 */
154
+ adName?: string;
155
+ }
156
+
157
+ /**
158
+ * 搜索结果
159
+ */
160
+ export interface SearchResult {
161
+ /** POI 列表 */
162
+ pois: POI[];
163
+ /** 总记录数 */
164
+ total: number;
165
+ /** 当前页码 */
166
+ pageNum: number;
167
+ /** 每页记录数 */
168
+ pageSize: number;
169
+ /** 总页数 */
170
+ pageCount: number;
171
+ }
172
+
173
+ /**
174
+ * 输入提示结果
175
+ */
176
+ export interface InputTipsResult {
177
+ /** 提示列表 */
178
+ tips: InputTip[];
179
+ }
@@ -0,0 +1,46 @@
1
+
2
+ import { requireNativeModule } from 'expo-modules-core';
3
+
4
+ /**
5
+ * 在加载原生搜索模块前,强制校验基础地图组件是否已安装。
6
+ * 支持两种“基础地图提供者”:expo-gaode-map 或 expo-gaode-map-navigation(导航内置地图)。
7
+ * 这样可避免导航与核心包的 SDK 冲突时无法使用搜索模块的问题。
8
+ */
9
+ function ensureBaseInstalled() {
10
+ let installed = false;
11
+ try {
12
+ // 优先检测核心地图包
13
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
14
+ require('expo-gaode-map');
15
+ installed = true;
16
+ } catch (_) {
17
+ // 再尝试导航包(内置地图能力)
18
+ try {
19
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
20
+ require('expo-gaode-map-navigation');
21
+ installed = true;
22
+ } catch (_) {
23
+ installed = false;
24
+ }
25
+ }
26
+
27
+ if (!installed) {
28
+ const msg =
29
+ '[expo-gaode-map-search] 需要先安装基础地图组件,支持以下任一包:\n' +
30
+ ' - expo-gaode-map(核心地图包),或\n' +
31
+ ' - expo-gaode-map-navigation(导航包,内置地图能力)\n' +
32
+ '请先安装并完成原生配置后再重试。';
33
+ throw new Error(msg);
34
+ }
35
+ }
36
+
37
+ ensureBaseInstalled();
38
+
39
+ /**
40
+ * 高德地图搜索模块
41
+ *
42
+ * 提供 POI 搜索、周边搜索、沿途搜索、多边形搜索和输入提示功能
43
+ */
44
+ const ExpoGaodeMapSearchModule = requireNativeModule('ExpoGaodeMapSearch');
45
+
46
+ export default ExpoGaodeMapSearchModule;
package/src/index.ts ADDED
@@ -0,0 +1,170 @@
1
+ import ExpoGaodeMapSearchModule from './ExpoGaodeMapSearchModule';
2
+ import type {
3
+ SearchType,
4
+ Coordinates,
5
+ POI,
6
+ POISearchOptions,
7
+ NearbySearchOptions,
8
+ AlongSearchOptions,
9
+ PolygonSearchOptions,
10
+ InputTipsOptions,
11
+ InputTip,
12
+ SearchResult,
13
+ InputTipsResult,
14
+ } from './ExpoGaodeMapSearch.types';
15
+
16
+ /**
17
+ * 初始化搜索模块(可选)
18
+ *
19
+ * 如果 API Key 已通过以下方式设置,则无需调用此方法:
20
+ * 1. app.json 的 plugins 中配置了 iosApiKey(推荐)
21
+ * 2. 调用了 ExpoGaodeMap.initSDK()
22
+ * 3. 在 AppDelegate 中手动设置
23
+ *
24
+ * 此方法会在首次调用搜索功能时自动执行,手动调用可以提前检测配置问题。
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { initSearch } from '@expo-gaode-map/search';
29
+ *
30
+ * // 可选:提前初始化以检测问题
31
+ * initSearch();
32
+ * ```
33
+ */
34
+ export function initSearch(): void {
35
+ ExpoGaodeMapSearchModule.initSearch();
36
+ }
37
+
38
+ /**
39
+ * POI 搜索
40
+ *
41
+ * @param options 搜索选项
42
+ * @returns 搜索结果
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const result = await searchPOI({
47
+ * keyword: '酒店',
48
+ * city: '北京',
49
+ * pageSize: 20,
50
+ * pageNum: 1,
51
+ * });
52
+ * console.log('找到', result.total, '个结果');
53
+ * result.pois.forEach(poi => {
54
+ * console.log(poi.name, poi.address);
55
+ * });
56
+ * ```
57
+ */
58
+ export async function searchPOI(options: POISearchOptions): Promise<SearchResult> {
59
+ return await ExpoGaodeMapSearchModule.searchPOI(options);
60
+ }
61
+
62
+ /**
63
+ * 周边搜索
64
+ *
65
+ * @param options 搜索选项
66
+ * @returns 搜索结果
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const result = await searchNearby({
71
+ * keyword: '餐厅',
72
+ * center: { latitude: 39.9, longitude: 116.4 },
73
+ * radius: 1000,
74
+ * });
75
+ * ```
76
+ */
77
+ export async function searchNearby(options: NearbySearchOptions): Promise<SearchResult> {
78
+ return await ExpoGaodeMapSearchModule.searchNearby(options);
79
+ }
80
+
81
+ /**
82
+ * 沿途搜索
83
+ *
84
+ * @param options 搜索选项
85
+ * @returns 搜索结果
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const result = await searchAlong({
90
+ * keyword: '加油站',
91
+ * polyline: [
92
+ * { latitude: 39.9, longitude: 116.4 },
93
+ * { latitude: 39.91, longitude: 116.41 },
94
+ * ],
95
+ * range: 500,
96
+ * });
97
+ * ```
98
+ */
99
+ export async function searchAlong(options: AlongSearchOptions): Promise<SearchResult> {
100
+ return await ExpoGaodeMapSearchModule.searchAlong(options);
101
+ }
102
+
103
+ /**
104
+ * 多边形搜索
105
+ *
106
+ * @param options 搜索选项
107
+ * @returns 搜索结果
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const result = await searchPolygon({
112
+ * keyword: '学校',
113
+ * polygon: [
114
+ * { latitude: 39.9, longitude: 116.4 },
115
+ * { latitude: 39.91, longitude: 116.4 },
116
+ * { latitude: 39.91, longitude: 116.41 },
117
+ * ],
118
+ * });
119
+ * ```
120
+ */
121
+ export async function searchPolygon(options: PolygonSearchOptions): Promise<SearchResult> {
122
+ return await ExpoGaodeMapSearchModule.searchPolygon(options);
123
+ }
124
+
125
+ /**
126
+ * 输入提示
127
+ *
128
+ * @param options 搜索选项
129
+ * @returns 提示结果
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const result = await getInputTips({
134
+ * keyword: '天安门',
135
+ * city: '北京',
136
+ * });
137
+ * result.tips.forEach(tip => {
138
+ * console.log(tip.name, tip.address);
139
+ * });
140
+ * ```
141
+ */
142
+ export async function getInputTips(options: InputTipsOptions): Promise<InputTipsResult> {
143
+ return await ExpoGaodeMapSearchModule.getInputTips(options);
144
+ }
145
+
146
+ // 导出类型和枚举
147
+ export type {
148
+ Coordinates,
149
+ POI,
150
+ POISearchOptions,
151
+ NearbySearchOptions,
152
+ AlongSearchOptions,
153
+ PolygonSearchOptions,
154
+ InputTipsOptions,
155
+ InputTip,
156
+ SearchResult,
157
+ InputTipsResult,
158
+ };
159
+
160
+ export { SearchType } from './ExpoGaodeMapSearch.types';
161
+
162
+ // 默认导出
163
+ export default {
164
+ initSearch,
165
+ searchPOI,
166
+ searchNearby,
167
+ searchAlong,
168
+ searchPolygon,
169
+ getInputTips,
170
+ };
package/.eslintrc.js DELETED
@@ -1,2 +0,0 @@
1
- // @generated by expo-module-scripts
2
- module.exports = require('expo-module-scripts/eslintrc.base.js');
@@ -1,49 +0,0 @@
1
- {
2
- "name": "expo-gaode-map-search",
3
- "version": "1.1.1-next.0",
4
- "description": "高德地图搜索功能模块 - POI搜索、关键词搜索、周边搜索,需先安装expo-gaode-map",
5
- "main": "build/index.js",
6
- "types": "build/index.d.ts",
7
- "scripts": {
8
- "build": "expo-module build",
9
- "clean": "expo-module clean",
10
- "lint": "expo-module lint",
11
- "test": "expo-module test",
12
- "prepare": "expo-module prepare",
13
- "prepublishOnly": "expo-module prepublishOnly",
14
- "expo-module": "expo-module",
15
- "postinstall": "node -e \"try{require.resolve('expo-gaode-map');process.exit(0)}catch(e1){try{require.resolve('expo-gaode-map-navigation');process.exit(0)}catch(e2){console.error('[expo-gaode-map-search] 需要安装基础地图组件:expo-gaode-map 或 expo-gaode-map-navigation 中的任意一个。\\n请执行:pnpm add expo-gaode-map 或 pnpm add expo-gaode-map-navigation');process.exit(1)}}\""
16
- },
17
- "keywords": [
18
- "react-native",
19
- "expo",
20
- "expo-gaode-map",
21
- "amap",
22
- "search",
23
- "poi",
24
- "高德地图",
25
- "搜索"
26
- ],
27
- "repository": "https://github.com/TomWq/expo-gaode-map",
28
- "bugs": {
29
- "url": "https://github.com/TomWq/expo-gaode-map/issues"
30
- },
31
- "author": "尚博信_王强 <wangqiang03@sunboxsoft.com> (https://github.com/TomWq)",
32
- "license": "MIT",
33
- "homepage": "https://github.com/TomWq/expo-gaode-map#readme",
34
- "dependencies": {
35
- "expo-gaode-map": "^2.2.2-next.0"
36
- },
37
- "devDependencies": {
38
- "@types/react": "~19.1.0",
39
- "expo": "^54.0.18",
40
- "expo-module-scripts": "^5.0.7",
41
- "react-native": "0.81.5",
42
- "typescript": "^5.9.3"
43
- },
44
- "peerDependencies": {
45
- "expo": "*",
46
- "react": "*",
47
- "react-native": "*"
48
- }
49
- }