clxx 3.0.1 → 3.0.4

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.
Files changed (55) hide show
  1. package/AGENTS.md +49 -3
  2. package/README.md +6 -3
  3. package/build/Alert/style.js +15 -14
  4. package/build/CarouselNotice/style.js +2 -1
  5. package/build/CitySelect/data.d.ts +1 -1
  6. package/build/CitySelect/data.js +2277 -1674
  7. package/build/CitySelect/index.d.ts +2 -1
  8. package/build/CitySelect/index.js +35 -9
  9. package/build/CitySelect/style.js +52 -48
  10. package/build/Container/index.js +21 -1
  11. package/build/DatePicker/style.d.ts +1 -1
  12. package/build/DatePicker/style.js +39 -35
  13. package/build/Indicator/index.js +2 -1
  14. package/build/Loading/Wrapper.js +3 -2
  15. package/build/Loading/style.js +7 -6
  16. package/build/MapLocationSelection/buildSelectedLocation.d.ts +16 -0
  17. package/build/MapLocationSelection/buildSelectedLocation.js +123 -0
  18. package/build/MapLocationSelection/createProvider.d.ts +8 -0
  19. package/build/MapLocationSelection/createProvider.js +33 -0
  20. package/build/MapLocationSelection/getLocation.d.ts +8 -0
  21. package/build/MapLocationSelection/getLocation.js +112 -0
  22. package/build/MapLocationSelection/index.d.ts +16 -0
  23. package/build/MapLocationSelection/index.js +985 -0
  24. package/build/MapLocationSelection/loader.amap.d.ts +48 -0
  25. package/build/MapLocationSelection/loader.amap.js +125 -0
  26. package/build/MapLocationSelection/loader.bmap.d.ts +8 -0
  27. package/build/MapLocationSelection/loader.bmap.js +60 -0
  28. package/build/MapLocationSelection/provider.amap.d.ts +38 -0
  29. package/build/MapLocationSelection/provider.amap.js +659 -0
  30. package/build/MapLocationSelection/provider.bmap.d.ts +36 -0
  31. package/build/MapLocationSelection/provider.bmap.js +837 -0
  32. package/build/MapLocationSelection/provider.d.ts +45 -0
  33. package/build/MapLocationSelection/provider.js +10 -0
  34. package/build/MapLocationSelection/style.d.ts +4 -0
  35. package/build/MapLocationSelection/style.js +442 -0
  36. package/build/MapLocationSelection/types.d.ts +29 -0
  37. package/build/MapLocationSelection/types.js +22 -0
  38. package/build/MapLocationSelection/userMarker.d.ts +2 -0
  39. package/build/MapLocationSelection/userMarker.js +95 -0
  40. package/build/RegionPicker/data.js +974 -992
  41. package/build/RegionPicker/index.d.ts +3 -2
  42. package/build/RegionPicker/index.js +29 -10
  43. package/build/RegionPicker/style.js +54 -49
  44. package/build/ScrollView/style.js +5 -4
  45. package/build/Toast/style.js +6 -5
  46. package/build/index.d.ts +3 -0
  47. package/build/index.js +8 -2
  48. package/build/utils/rem.d.ts +1 -0
  49. package/build/utils/rem.js +48 -0
  50. package/package.json +2 -2
  51. package/test/src/city-select/index.jsx +28 -15
  52. package/test/src/index/index.jsx +5 -0
  53. package/test/src/index.jsx +1 -0
  54. package/test/src/map-location-selection/index.jsx +192 -0
  55. package/test/src/region-picker/index.jsx +29 -21
@@ -0,0 +1,659 @@
1
+ "use strict";
2
+ // 高德地图 Provider 实现。
3
+ //
4
+ // 这个文件是从原 index.tsx 直接拆出来的:所有 AMap 相关 API 调用、
5
+ // NEARBY_POI_TYPE、expandWithChildren、normalizePOI、Geolocation 单例化、
6
+ // hasMore 计算(`reachedFullPage && targetPage * pageSize < count`)等
7
+ // 全部内聚在这里,UI 层完全不再 import "AMap*"。
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.AMapProvider = void 0;
19
+ const loader_amap_1 = require("./loader.amap");
20
+ const types_1 = require("./types");
21
+ const userMarker_1 = require("./userMarker");
22
+ const DEFAULT_FALLBACK_CENTER_GCJ02 = [116.397428, 39.90923]; // 北京
23
+ // IP 定位判定阈值:accuracy ≥ 此值视为 IP 兜底(城市级精度)。
24
+ // 室内 H5/WiFi 定位精度再差也很少超过 2km,5km 完全是城市级 IP 才有的精度,
25
+ // 与百度端 isIpLikeBmapAccuracy 阈值保持一致。
26
+ const IP_LOCATION_ACCURACY_THRESHOLD_M = 5000;
27
+ // 高德 IP 兜底事后判定。
28
+ //
29
+ // 字段优先级:
30
+ // 1) result.location_type:高德 SDK 内部字段,'ip' / 'cgi' 表示非 GPS。
31
+ // 官方文档未在公开 API 表中列出,但实际运行时存在(v2.0 至今稳定);
32
+ // 2) accuracy 阈值兜底:location_type 缺省 / 字段名变更时仍能识别。
33
+ function isIpLikeAmapResult(result) {
34
+ var _a;
35
+ const t = ((_a = result === null || result === void 0 ? void 0 : result.location_type) !== null && _a !== void 0 ? _a : "").toString().toLowerCase();
36
+ if (t === "ip" || t === "cgi")
37
+ return true;
38
+ const acc = Number(result === null || result === void 0 ? void 0 : result.accuracy);
39
+ return Number.isFinite(acc) && acc >= IP_LOCATION_ACCURACY_THRESHOLD_M;
40
+ }
41
+ // 高德 PlaceSearch 在 keyword 为空时,必须依靠 type 才会返回附近 POI。
42
+ // 这里把所有大类编码全部打开,确保「楼栋 / 门牌地址 / 室内设施 / 公共厕所
43
+ // 等」全部召回,否则附近列表会只到“小区”这一级,丢掉具体的楼栋 / 加油站
44
+ // 厕所 / 商场内店铺等子粒度 POI。
45
+ //
46
+ // 高德官方 24 个一级大类对照(**全部打开**,配合 extensions=all + child_pois
47
+ // 可达"楼栋/楼层/出入口/公厕"等子 POI 极细粒度):
48
+ // 010000 汽车服务(加油站、充电站) 020000 汽车销售
49
+ // 030000 汽车维修 040000 摩托车服务
50
+ // 050000 餐饮服务 060000 购物服务
51
+ // 070000 生活服务 080000 体育休闲
52
+ // 090000 医疗保健 100000 住宿服务(酒店)
53
+ // 110000 风景名胜 120000 商务住宅(小区/楼栋)
54
+ // 130000 政府机构 140000 科教文化
55
+ // 150000 交通设施(地铁站/出入口) 160000 金融保险
56
+ // 170000 公司企业(写字楼) 180000 道路附属设施
57
+ // 190000 地名地址(门牌/村庄) 200000 公共设施(公厕/邮筒/报刊亭)
58
+ // 220000 室内设施(楼层/出入口/电梯) 970000 通行设施
59
+ // 990000 事件活动
60
+ const NEARBY_POI_TYPE = [
61
+ "010000",
62
+ "020000",
63
+ "030000",
64
+ "040000",
65
+ "050000",
66
+ "060000",
67
+ "070000",
68
+ "080000",
69
+ "090000",
70
+ "100000",
71
+ "110000",
72
+ "120000",
73
+ "130000",
74
+ "140000",
75
+ "150000",
76
+ "160000",
77
+ "170000",
78
+ "180000",
79
+ "190000",
80
+ "200000",
81
+ "220000",
82
+ "970000",
83
+ "990000",
84
+ ].join("|");
85
+ // 把父 POI 与其 child_pois 展开成一个扁平数组(父在前,子按原顺序紧随其后)。
86
+ // 子 POI 通常缺少 distance / cityname,这里基于父 POI 兜底,让排序与展示一致。
87
+ function expandWithChildren(pois) {
88
+ var _a, _b, _c;
89
+ const out = [];
90
+ for (const p of pois) {
91
+ if (!p)
92
+ continue;
93
+ out.push(p);
94
+ const children = Array.isArray(p.child_pois) ? p.child_pois : [];
95
+ for (const c of children) {
96
+ if (!c || !c.location)
97
+ continue;
98
+ out.push(Object.assign(Object.assign({}, c), { cityname: (_a = c.cityname) !== null && _a !== void 0 ? _a : p.cityname, pname: (_b = c.pname) !== null && _b !== void 0 ? _b : p.pname, adname: (_c = c.adname) !== null && _c !== void 0 ? _c : p.adname, distance: typeof c.distance === "number"
99
+ ? c.distance
100
+ : typeof p.distance === "number"
101
+ ? p.distance
102
+ : undefined,
103
+ // 子 POI 的 address 经常为空,回退到父 POI 名称作为上下文
104
+ address: c.address || p.name || p.address || "" }));
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+ function normalizePOI(poi) {
110
+ var _a, _b, _c, _d;
111
+ if (!poi || !poi.location)
112
+ return null;
113
+ const lng = typeof poi.location.getLng === "function"
114
+ ? poi.location.getLng()
115
+ : poi.location.lng;
116
+ const lat = typeof poi.location.getLat === "function"
117
+ ? poi.location.getLat()
118
+ : poi.location.lat;
119
+ if (typeof lng !== "number" || typeof lat !== "number")
120
+ return null;
121
+ return {
122
+ id: (_a = poi.id) !== null && _a !== void 0 ? _a : `${lng},${lat},${(_b = poi.name) !== null && _b !== void 0 ? _b : ""}`,
123
+ name: (_c = poi.name) !== null && _c !== void 0 ? _c : "",
124
+ address: (_d = poi.address) !== null && _d !== void 0 ? _d : "",
125
+ location: { lng, lat },
126
+ cityname: poi.cityname,
127
+ pname: poi.pname,
128
+ adname: poi.adname,
129
+ distance: typeof poi.distance === "number" ? poi.distance : undefined,
130
+ raw: poi,
131
+ };
132
+ }
133
+ class AMapProvider {
134
+ constructor(opts) {
135
+ this.AMap = null;
136
+ this.map = null;
137
+ // 周边检索专用:init 时设 type=NEARBY_POI_TYPE 让"附近列表"覆盖楼宇/门牌等大类,
138
+ // extensions=all 才返回 child_pois(楼栋粒度)。
139
+ this.placeSearch = null;
140
+ // 关键字检索:双实例并发,覆盖两类用户意图,二者结果合并去重 + 50km 半径过滤
141
+ // 后回给 UI 层(UI 再用「名称命中度优先 + 同档距离升序」做最终排序)。
142
+ //
143
+ // 单实例(仅 searchNearBy)的痛点:
144
+ // * searchNearBy 是「半径内 + 关键字宽松匹配 + 按距离排」。高德 SDK 内部对
145
+ // "虹桥火车站"拆 token,"虹桥"、"火车站"任一片段命中即收录——浦东虹桥花园
146
+ // 里的充电站等近距离模糊命中会把 pageSize=20 的首页全部占满,**真实
147
+ // 虹桥火车站(28km 外)根本进不了候选**,用户感觉"列表里完全没有想搜的";
148
+ // * 单纯 ps.search(kw) 又会被"锦博苑"等非热门小区在全国相关性排序里挤掉
149
+ // (北京同名 POI 热度更高时会跑到前面),且 search 不限半径会拿到外地结果。
150
+ //
151
+ // 双实例并发(实测能解 99% 的"搜不到真实 POI"问题):
152
+ // * keywordPlaceSearchNear(searchNearBy)拿"近距离命中"——锦博苑/楼宇/
153
+ // 近距离模糊匹配等都从这里来;
154
+ // * keywordPlaceSearchFull(search)拿"全国相关性命中"——虹桥火车站/陆家
155
+ // 嘴等高热精确匹配 POI 即便远在 28km 外也能稳定进入候选;
156
+ // * 二者各取 pageSize=50(SDK 上限)、extensions=all、不设 type 让 keyword
157
+ // 当主过滤;不设 cityLimit(city="全国")不加城市约束;
158
+ // * 合并后用 haversine 重算距离并丢弃 > 50km 的跨市残留(search 会带回北京
159
+ // /广州的同名 POI,距离过滤天然解决);
160
+ // * 名称命中度优先排序由 UI 层 sortByKeywordRelevance 完成(与百度共用),
161
+ // 这里只负责"把候选凑齐"。
162
+ //
163
+ // 与单实例相比的代价:每次关键字检索多打一次 SDK 调用(约 +200~500ms 网络),
164
+ // 已被 UI 层 250ms debounce 削平至单次输入只触发一组并发请求;调用配额翻倍
165
+ // 但 keyword 检索本身在打车流程中频次很低,整体配额压力可忽略。
166
+ this.keywordPlaceSearchNear = null;
167
+ this.keywordPlaceSearchFull = null;
168
+ this.geocoder = null;
169
+ this.geolocation = null;
170
+ this.userMarker = null;
171
+ // 复用一次进行中的定位 Promise,去抖
172
+ this.pendingGeolocate = null;
173
+ // 周边搜索 / 关键字搜索 各自独立的 seq,过期请求直接丢弃
174
+ this.aroundSeq = 0;
175
+ this.keywordSeq = 0;
176
+ // ===== 跨城 flyTo 状态(v2 新增)=====
177
+ // 高德 JSAPI 没有原生 flyTo(百度有 map.flyTo),setZoomAndCenter 默认只是
178
+ // "直线平移 + 同步缩放",跨城(如上海点北京天安门,距离 1000+ km)时,途中
179
+ // 以高 zoom 沿途经过大量瓦片,瓦片下载量爆炸 + 地图长时间空白。
180
+ // 这里手动模拟 Mapbox flyTo 风格的"先缩小 → 平移 → 再放大"三段式动画,
181
+ // 让中间段在低 zoom(5-8 级)下平移,瓦片密度极低,请求量与空白时间都收敛。
182
+ // flying = true 期间 SDK 触发的 movestart / moveend 都被 on() wrapper 屏蔽——
183
+ // 让上层 programmaticMoveRef 单次 flag 设计无需感知内部多段动画细节。
184
+ this.flying = false;
185
+ this.flyToTimers = [];
186
+ this.opts = opts;
187
+ }
188
+ // 加载 SDK + 创建所有 service 类(Geocoder / PlaceSearch ×3 / Geolocation
189
+ // 单例延迟到 geolocate 内部按需创建)。
190
+ // init() 在本方法之后创建 Map;initHeadless() 到此为止(getLocation 用)。
191
+ // 抽出来共用是因为两条路径下 service 类的构造完全相同。
192
+ initServices(initialCity) {
193
+ return __awaiter(this, void 0, void 0, function* () {
194
+ const AMap = yield (0, loader_amap_1.loadAMap)({
195
+ key: this.opts.amapKey,
196
+ securityJsCode: this.opts.securityJsCode,
197
+ plugins: ["AMap.Geocoder", "AMap.PlaceSearch", "AMap.Geolocation"],
198
+ });
199
+ this.AMap = AMap;
200
+ const city = initialCity !== null && initialCity !== void 0 ? initialCity : "全国";
201
+ this.placeSearch = new AMap.PlaceSearch({
202
+ pageSize: 20,
203
+ pageIndex: 1,
204
+ // extensions=all 才会返回 child_pois(楼栋等子 POI),
205
+ // 否则即便 type 命中"商务住宅"也只到小区粒度。
206
+ extensions: "all",
207
+ type: NEARBY_POI_TYPE,
208
+ city,
209
+ });
210
+ // 关键字检索双实例:分别给 searchNearBy / search 用,避免共享实例时 setPageSize
211
+ // 等状态被并发调用相互覆盖。pageSize 拉满到 50(SDK 上限)尽量多召回候选,
212
+ // 让 UI 端的"名称命中度优先排序"有更多素材可挑。
213
+ this.keywordPlaceSearchNear = new AMap.PlaceSearch({
214
+ pageSize: 50,
215
+ pageIndex: 1,
216
+ extensions: "all",
217
+ city,
218
+ });
219
+ this.keywordPlaceSearchFull = new AMap.PlaceSearch({
220
+ pageSize: 50,
221
+ pageIndex: 1,
222
+ extensions: "all",
223
+ city,
224
+ });
225
+ // extensions=all 是关键:让 regeocode 一并返回 pois 数组(带 distance 字段)。
226
+ // 默认值在不同 JSAPI 版本下会变(v1 默认 base / v2 默认 all),显式声明
227
+ // 避免被 SDK 默认值变化打脸——reverseGeocode 内部需要 pois[].distance
228
+ // 才能挑出"30m 内最贴近 centerPin 的精确 POI"。
229
+ this.geocoder = new AMap.Geocoder({ extensions: "all" });
230
+ return AMap;
231
+ });
232
+ }
233
+ init(o) {
234
+ return __awaiter(this, void 0, void 0, function* () {
235
+ var _a, _b;
236
+ const AMap = yield this.initServices(o.initialCity);
237
+ const center = (_a = o.initialCenter) !== null && _a !== void 0 ? _a : DEFAULT_FALLBACK_CENTER_GCJ02;
238
+ const zoom = (_b = o.initialZoom) !== null && _b !== void 0 ? _b : 16;
239
+ this.map = new AMap.Map(o.container, {
240
+ viewMode: "2D",
241
+ zoom,
242
+ center,
243
+ showLabel: true,
244
+ });
245
+ });
246
+ }
247
+ // headless:仅 service 类,不创建 Map 实例 → 不挂容器 / 不下载瓦片 /
248
+ // 不渲染。详见 MapProvider.initHeadless 接口注释。
249
+ initHeadless(opts) {
250
+ return __awaiter(this, void 0, void 0, function* () {
251
+ yield this.initServices(opts === null || opts === void 0 ? void 0 : opts.initialCity);
252
+ });
253
+ }
254
+ destroy() {
255
+ // 先清 flyTo 的 setTimeout,避免段 2 / 段 3 在地图已销毁后仍尝试调用
256
+ // this.map.setCenter / setZoom 抛 NPE。
257
+ this.flyToTimers.forEach((id) => window.clearTimeout(id));
258
+ this.flyToTimers = [];
259
+ this.flying = false;
260
+ try {
261
+ if (this.map)
262
+ this.map.destroy();
263
+ }
264
+ catch (_a) {
265
+ // ignore
266
+ }
267
+ this.AMap = null;
268
+ this.map = null;
269
+ this.placeSearch = null;
270
+ this.keywordPlaceSearchNear = null;
271
+ this.keywordPlaceSearchFull = null;
272
+ this.geocoder = null;
273
+ this.geolocation = null;
274
+ this.userMarker = null;
275
+ this.pendingGeolocate = null;
276
+ }
277
+ getCenter() {
278
+ const c = this.map.getCenter();
279
+ return [c.getLng(), c.getLat()];
280
+ }
281
+ // 短距离:保持原 setZoomAndCenter / setCenter 行为(一段直线平移)。
282
+ // 跨城(>= 50km):走 flyTo 三段式动画——先缩小到能一屏看到起终点的 zoom、
283
+ // 在低 zoom 下平移、再放大到目标 zoom。可大幅降低瓦片请求量与地图空白时间,
284
+ // 体感与百度 BMapGL.Map.flyTo / 微信"发送位置"跨城跳转一致。
285
+ setCenter(center, zoom) {
286
+ if (!this.map)
287
+ return;
288
+ const c = this.map.getCenter();
289
+ const distanceM = (0, types_1.haversineMeters)(c.getLng(), c.getLat(), center[0], center[1]);
290
+ // 50km 阈值兼顾"市内跨区"与"跨城":
291
+ // * 市内拖图 / 列表点选附近 POI 距离普遍 < 30km,不应触发"先缩小再放大"
292
+ // 的鸟瞰动画(反而显得卡顿绕路);
293
+ // * 50km 以上基本是"省内跨市"或"跨省",瓦片直线穿越的成本足够高,三段式
294
+ // 动画的体感收益开始回正。
295
+ const SHORT_RANGE_M = 50000;
296
+ if (distanceM < SHORT_RANGE_M) {
297
+ if (typeof zoom === "number") {
298
+ this.map.setZoomAndCenter(zoom, [center[0], center[1]]);
299
+ }
300
+ else {
301
+ this.map.setCenter([center[0], center[1]]);
302
+ }
303
+ return;
304
+ }
305
+ // zoom 缺省时取当前 zoom 作为目标——保持调用方"不传 zoom = 不改 zoom"的语义。
306
+ const targetZoom = typeof zoom === "number" ? zoom : this.map.getZoom();
307
+ this.flyTo([center[0], center[1]], targetZoom, distanceM);
308
+ }
309
+ // 三段式 flyTo(手动模拟,因为高德 JSAPI 2.0 没有原生 flyTo 接口)。
310
+ //
311
+ // 时序(总耗时 ≈ 990ms,与百度 flyTo 体感对齐):
312
+ // t=0 setZoomAndCenter(midZoom, current, 250) // 段 1:原地缩小
313
+ // t=270 setCenter(target, 350) // 段 2:低 zoom 平移
314
+ // t=640 flying=false → setZoom(targetZoom, 350) // 段 3:放大到目标
315
+ // t=990 段 3 动画结束 → 真实 moveend 上抛 → 上层正常 commit + drop pin
316
+ //
317
+ // midZoom 选择策略:距离越远越缩小,让平移阶段瓦片密度尽可能低。
318
+ // 1000km+ → zoom 5(全国一屏)
319
+ // 500-1000 → zoom 6
320
+ // 200-500 → zoom 7
321
+ // 50-200 → zoom 8(省内跨市)
322
+ //
323
+ // 段 1 / 段 2 触发的 SDK moveend 被 on() wrapper 按 this.flying 屏蔽;段 3
324
+ // 启动前清掉 flying,让段 3 自然产生的 moveend 正常上抛——上层
325
+ // programmaticMoveRef 设计是"单次消费",只期待一次 moveend。
326
+ //
327
+ // 取消语义:连续 setCenter 时新调用先清掉旧的 flyToTimers,由新一轮 flyTo
328
+ // 接管——避免多次连续点击跨城 POI 时段动画堆叠到错乱位置。
329
+ flyTo(target, targetZoom, distanceM) {
330
+ if (!this.map)
331
+ return;
332
+ this.flyToTimers.forEach((id) => window.clearTimeout(id));
333
+ this.flyToTimers = [];
334
+ let midZoom = 8;
335
+ if (distanceM >= 1000000)
336
+ midZoom = 5;
337
+ else if (distanceM >= 500000)
338
+ midZoom = 6;
339
+ else if (distanceM >= 200000)
340
+ midZoom = 7;
341
+ const c = this.map.getCenter();
342
+ const currentLng = c.getLng();
343
+ const currentLat = c.getLat();
344
+ this.flying = true;
345
+ this.map.setZoomAndCenter(midZoom, [currentLng, currentLat], false, 250);
346
+ this.flyToTimers.push(window.setTimeout(() => {
347
+ if (!this.map)
348
+ return;
349
+ this.map.setCenter([target[0], target[1]], false, 350);
350
+ }, 270));
351
+ this.flyToTimers.push(window.setTimeout(() => {
352
+ if (!this.map)
353
+ return;
354
+ // 段 3 启动前先解屏:本段自然结束触发的 moveend 会到达上层。
355
+ this.flying = false;
356
+ this.map.setZoom(targetZoom, false, 350);
357
+ }, 640));
358
+ }
359
+ upsertUserMarker(center) {
360
+ const AMap = this.AMap;
361
+ if (!AMap || !this.map)
362
+ return;
363
+ if (!this.userMarker) {
364
+ const content = (0, userMarker_1.createUserMarkerDom)();
365
+ this.userMarker = new AMap.Marker({
366
+ position: [center[0], center[1]],
367
+ content,
368
+ // content 是 0x0 的锚点 wrap(详见 userMarker.ts):默认 top-left 对齐时
369
+ // wrap 左上角就是 position;内部真实可见 marker 已用 absolute -6/-6 自我居中。
370
+ // 所以这里 offset 必须是 (0, 0),再叠加 -6 反而会双重偏移把蓝点拉到 position 左上方。
371
+ offset: new AMap.Pixel(0, 0),
372
+ zIndex: 90,
373
+ clickable: false,
374
+ bubble: true,
375
+ });
376
+ this.userMarker.setMap(this.map);
377
+ }
378
+ else {
379
+ this.userMarker.setPosition([center[0], center[1]]);
380
+ }
381
+ }
382
+ searchAround(center, options) {
383
+ var _a;
384
+ const AMap = this.AMap;
385
+ const ps = this.placeSearch;
386
+ if (!AMap || !ps) {
387
+ return Promise.resolve({ pois: [], hasMore: false });
388
+ }
389
+ const page = Math.max(1, (_a = options.page) !== null && _a !== void 0 ? _a : 1);
390
+ const pageSize = options.pageSize;
391
+ const seq = ++this.aroundSeq;
392
+ return new Promise((resolve) => {
393
+ ps.setPageIndex(page);
394
+ ps.setPageSize(pageSize);
395
+ // 高德 searchNearBy 文档明确 radius 取值 [0, 50000]:业务方传超过 50000
396
+ // 会触发 SDK 报错(status='error'),这里先 clamp 兜住。
397
+ const radius = Math.min(Math.max(0, options.radius), 50000);
398
+ ps.searchNearBy("", new AMap.LngLat(center[0], center[1]), radius, (status, result) => {
399
+ var _a, _b, _c, _d;
400
+ if (seq !== this.aroundSeq) {
401
+ // 请求已被作废
402
+ resolve({ pois: [], hasMore: false });
403
+ return;
404
+ }
405
+ const rawPois = (_b = (_a = result === null || result === void 0 ? void 0 : result.poiList) === null || _a === void 0 ? void 0 : _a.pois) !== null && _b !== void 0 ? _b : [];
406
+ const pois = expandWithChildren(rawPois)
407
+ .map(normalizePOI)
408
+ .filter((x) => !!x);
409
+ // 高德 PlaceSearch 返回的 count 只统计父 POI,而 list 是展开 child_pois 后的扁平数组,
410
+ // 当区域内 child_pois 多时 list.length 会远大于 count,用 list.length < count 判断
411
+ // 「是否还有下一页」会永远 false,分页完全失效。
412
+ // 这里用「本次接口返回的原始 pois 是否取满一页」作为判断依据:
413
+ // - 满一页(rawPois.length >= pageSize)→ 大概率还有下一页
414
+ // - 不满一页 → 已是最后一页(高德接口约定)
415
+ // 同时叠加 totalCount 兜底(page * pageSize < count),避免接口偶尔返回多余数据。
416
+ const totalCount = (_d = (_c = result === null || result === void 0 ? void 0 : result.poiList) === null || _c === void 0 ? void 0 : _c.count) !== null && _d !== void 0 ? _d : (status === "complete" ? rawPois.length : 0);
417
+ const reachedFullPage = rawPois.length >= pageSize;
418
+ const hasMoreByCount = page * pageSize < totalCount;
419
+ const hasMore = reachedFullPage && hasMoreByCount;
420
+ if (status !== "complete" || pois.length === 0) {
421
+ // 诊断:status='error' 大多是安全密钥未配置或服务报错
422
+ if (status === "error") {
423
+ console.warn("[MapLocationSelection] AMap PlaceSearch 失败。常见原因:" +
424
+ "未配置 securityJsCode(JSAPI v2.0 必填) / Key 未开通服务 / 超出限额。info=", result === null || result === void 0 ? void 0 : result.info);
425
+ }
426
+ resolve({ pois: [], hasMore: false });
427
+ return;
428
+ }
429
+ resolve({ pois, hasMore });
430
+ });
431
+ });
432
+ }
433
+ searchByKeyword(center, keyword, options) {
434
+ return __awaiter(this, void 0, void 0, function* () {
435
+ // 关键字检索 = 「searchNearBy(近距离命中)」+「search(全国相关性命中)」并发,
436
+ // 合并去重后**全部**返回,**不做距离过滤**。
437
+ //
438
+ // 旧版本曾在末尾做 50km 半径过滤以剔除同名跨市 POI("北京/广州的锦博苑")。
439
+ // 但实测这条过滤会误杀用户实际想找的远端地标——例如在上海搜"天安门",全国
440
+ // 唯一的精确命中就是 1075km 外的北京天安门,被 50km 切掉后整个列表为空,
441
+ // 与微信"发送位置"的体感(远端命中显示 1074.5km)严重不符。
442
+ //
443
+ // 排序口径:本方法只负责把候选凑齐,**不在这里排序**——UI 层
444
+ // sortByKeywordRelevance 用「名称包含完整关键字优先 + 同档距离升序」做最终排序,
445
+ // 同城精确命中("上海锦博苑")凭距离稳居顶部,同名跨市命中(北京/广州)
446
+ // 自然沉到列表末尾;而对于唯一远端命中("天安门" → 北京),它顶到首位
447
+ // 是用户期望的行为。距离过滤反而是误杀。
448
+ const AMap = this.AMap;
449
+ const psNear = this.keywordPlaceSearchNear;
450
+ const psFull = this.keywordPlaceSearchFull;
451
+ if (!AMap || !psNear || !psFull)
452
+ return [];
453
+ const seq = ++this.keywordSeq;
454
+ const pageSize = Math.min(50, Math.max(20, options.pageSize));
455
+ const runNear = () => new Promise((resolve) => {
456
+ psNear.setPageIndex(1);
457
+ psNear.setPageSize(pageSize);
458
+ psNear.searchNearBy(keyword, new AMap.LngLat(center[0], center[1]), 50000, (status, result) => {
459
+ var _a;
460
+ if (status !== "complete" || !((_a = result === null || result === void 0 ? void 0 : result.poiList) === null || _a === void 0 ? void 0 : _a.pois)) {
461
+ resolve([]);
462
+ return;
463
+ }
464
+ const list = expandWithChildren(result.poiList.pois)
465
+ .map(normalizePOI)
466
+ .filter((x) => !!x);
467
+ resolve(list);
468
+ });
469
+ });
470
+ const runFull = () => new Promise((resolve) => {
471
+ psFull.setPageIndex(1);
472
+ psFull.setPageSize(pageSize);
473
+ // search(kw) 不限半径、不限城市:高热 POI(虹桥火车站 / 陆家嘴 / 东方明珠
474
+ // 等)会稳定排在 search 结果首页,**远端唯一精确命中也会被召回**——
475
+ // 例如在上海搜"天安门",能拿到 1075km 外的北京天安门,跟微信"发送
476
+ // 位置"的体感一致。与 runNear 合并去重后**全部**返回,不在 provider
477
+ // 层做距离过滤;UI 层 sortByKeywordRelevance 会按"名称命中度分级 +
478
+ // 同档距离升序"排序,跨城远端命中自然排在同城精确命中之后。
479
+ psFull.search(keyword, (status, result) => {
480
+ var _a;
481
+ if (status !== "complete" || !((_a = result === null || result === void 0 ? void 0 : result.poiList) === null || _a === void 0 ? void 0 : _a.pois)) {
482
+ resolve([]);
483
+ return;
484
+ }
485
+ const list = expandWithChildren(result.poiList.pois)
486
+ .map(normalizePOI)
487
+ .filter((x) => !!x);
488
+ resolve(list);
489
+ });
490
+ });
491
+ const [nearby, fulltext] = yield Promise.all([runNear(), runFull()]);
492
+ if (seq !== this.keywordSeq)
493
+ return [];
494
+ // 合并去重:以 nearby 优先(更可能带 SDK 算好的 distance),fulltext 仅补
495
+ // nearby 漏掉的远端高热精确命中。同 id 用 nearby 的版本,避免 search 不带
496
+ // distance 的副本覆盖 nearby 已经拿到的距离信息。
497
+ const seen = new Map();
498
+ for (const p of nearby)
499
+ seen.set(p.id, p);
500
+ for (const p of fulltext) {
501
+ if (!seen.has(p.id))
502
+ seen.set(p.id, p);
503
+ }
504
+ return Array.from(seen.values());
505
+ });
506
+ }
507
+ // 定位:AMap.Geolocation.getCurrentPosition(官方推荐方式)。
508
+ //
509
+ // ⚠️ IP 兜底事后判定(与 noIpLocate 取舍):
510
+ // 高德 v1.4 提供过 `noIpLocate` 构造参数,但 v2 下已不再可靠(部分版本静默
511
+ // 忽略),且业务层希望"是否允许 IP 兜底"做成**调用时**的开关而非"实例级"
512
+ // 配置(同一个 provider 实例下不同调用可以选择不同策略)。所以这里**不再
513
+ // 传 noIpLocate**,统一走"拿到 result 后看 location_type / accuracy 事后判定"
514
+ // 的路径——既兼容所有 SDK 版本,也支持调用时灵活切换。
515
+ //
516
+ // 判定规则(详见 isIpLikeAmapResult):
517
+ // * result.location_type === 'ip' / 'cgi' → 视为 IP 定位
518
+ // * accuracy ≥ 5000m → 兜底视为 IP(5km 阈值远超室内 H5 误差上限)
519
+ geolocate(options) {
520
+ const AMap = this.AMap;
521
+ if (!AMap)
522
+ return Promise.resolve(null);
523
+ // in-flight 复用:避免连续点击触发多次 GPS 弹权限框。同一 provider 实例
524
+ // 上不会出现"a 调用 allowIpFallback=true、b 调用 allowIpFallback=false"
525
+ // 的并发——业务方一次决策,全程一致——所以此处不区分 options 复用即可。
526
+ if (this.pendingGeolocate)
527
+ return this.pendingGeolocate;
528
+ if (!this.geolocation) {
529
+ this.geolocation = new AMap.Geolocation({
530
+ enableHighAccuracy: true,
531
+ timeout: 10000,
532
+ // convert: true 让 SDK 把原始 WGS84 转成 GCJ02,与 provider 坐标系自洽。
533
+ convert: true,
534
+ // 关闭高德自带 UI,由组件接管
535
+ showButton: false,
536
+ showMarker: false,
537
+ showCircle: false,
538
+ panToLocation: false,
539
+ zoomToAccuracy: false,
540
+ });
541
+ }
542
+ const allowIp = (options === null || options === void 0 ? void 0 : options.allowIpFallback) === true;
543
+ const promise = new Promise((resolve) => {
544
+ this.geolocation.getCurrentPosition((status, result) => {
545
+ if (status !== "complete" || !(result === null || result === void 0 ? void 0 : result.position)) {
546
+ console.warn("[MapLocationSelection] AMap Geolocation 失败,请确认 https/localhost 环境与授权。status=", status, "result=", result);
547
+ resolve(null);
548
+ return;
549
+ }
550
+ if (!allowIp && isIpLikeAmapResult(result)) {
551
+ console.warn("[MapLocationSelection] AMap Geolocation 命中 IP 兜底(location_type=", result.location_type, "accuracy=", result.accuracy, ")但 allowIpFallback=false,已丢弃返回 null。");
552
+ resolve(null);
553
+ return;
554
+ }
555
+ resolve([result.position.getLng(), result.position.getLat()]);
556
+ });
557
+ }).finally(() => {
558
+ this.pendingGeolocate = null;
559
+ });
560
+ this.pendingGeolocate = promise;
561
+ return promise;
562
+ }
563
+ reverseGeocode(center) {
564
+ const geocoder = this.geocoder;
565
+ if (!geocoder)
566
+ return Promise.resolve(null);
567
+ return new Promise((resolve) => {
568
+ geocoder.getAddress([center[0], center[1]], (status, result) => {
569
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
570
+ if (status === "complete" && (result === null || result === void 0 ? void 0 : result.regeocode)) {
571
+ const r = result.regeocode;
572
+ const city = ((_a = r.addressComponent) === null || _a === void 0 ? void 0 : _a.city) || ((_b = r.addressComponent) === null || _b === void 0 ? void 0 : _b.province) || "";
573
+ // reverseGeocode 只负责"地名兜底"——返回覆盖中心点的具体地名
574
+ // (楼宇 / 街道+门牌 / 街道 / 镇)。**不掺 POI**,POI 借用由
575
+ // commitMapCenter 用 fetchAround(PlaceSearch.searchNearBy 全大类)
576
+ // 的结果统一处理,因为 regeo.pois 是按"重要性"筛过的 N 条,常
577
+ // 漏掉酒店/楼宇等具体 POI(实测:"全季酒店"在地图上明显贴 centerPin
578
+ // 但 regeo.pois 里根本没有,导致 fallback 到 township="北蔡镇")。
579
+ //
580
+ // 文档参考:
581
+ // https://lbs.amap.com/api/webservice/guide/api/georegeo
582
+ //
583
+ // name 候选优先级(精到粗):
584
+ // 楼宇.name(extensions=all 才有)→ 街道+门牌 → 街道 → 街道乡镇 → 商圈
585
+ // address 候选:
586
+ // 街道+门牌 → 街道 → formattedAddress
587
+ const ac = (_c = r.addressComponent) !== null && _c !== void 0 ? _c : {};
588
+ const businessAreas = Array.isArray(ac.businessAreas)
589
+ ? ac.businessAreas
590
+ : [];
591
+ const sn = (_d = ac.streetNumber) !== null && _d !== void 0 ? _d : {};
592
+ const street = ((_e = sn.street) !== null && _e !== void 0 ? _e : "").toString().trim();
593
+ const number = ((_f = sn.number) !== null && _f !== void 0 ? _f : "").toString().trim();
594
+ const streetAndNumber = street && number ? `${street}${number}` : "";
595
+ const township = ((_g = ac.township) !== null && _g !== void 0 ? _g : "").toString().trim();
596
+ const formatted = ((_h = r.formattedAddress) !== null && _h !== void 0 ? _h : "").toString().trim();
597
+ const nameCandidates = [
598
+ (_j = ac.building) === null || _j === void 0 ? void 0 : _j.name,
599
+ streetAndNumber,
600
+ street,
601
+ township,
602
+ (_k = businessAreas[0]) === null || _k === void 0 ? void 0 : _k.name,
603
+ ];
604
+ const name = (_l = nameCandidates
605
+ .map((s) => (s !== null && s !== void 0 ? s : "").toString().trim())
606
+ .find(Boolean)) !== null && _l !== void 0 ? _l : "";
607
+ const addressCandidates = [
608
+ streetAndNumber,
609
+ street,
610
+ township,
611
+ formatted,
612
+ ];
613
+ const addressRaw = (_m = addressCandidates.find(Boolean)) !== null && _m !== void 0 ? _m : "";
614
+ // name / address 落到同一候选时会完全重复,让 address 留空(描述行
615
+ // 只显示距离),跟周边 POI 项 "address 为空" 的渲染分支保持一致。
616
+ const address = addressRaw === name ? "" : addressRaw;
617
+ // 高德 r.addressComponent.adcode 是 6 位国标区县码(如 310115),
618
+ // 与 GB/T 2260 完全对齐。直辖市 / 不设区地级市的 corner case 由
619
+ // UI 层 splitAdcode 处理(参考 SelectedLocation.provinceCode 注释)。
620
+ const adcodeRaw = ((_o = ac.adcode) !== null && _o !== void 0 ? _o : "").toString().trim();
621
+ const adcode = /^\d{6}$/.test(adcodeRaw) ? adcodeRaw : undefined;
622
+ resolve({
623
+ name,
624
+ address,
625
+ province: (_p = r.addressComponent) === null || _p === void 0 ? void 0 : _p.province,
626
+ city: city || undefined,
627
+ district: (_q = r.addressComponent) === null || _q === void 0 ? void 0 : _q.district,
628
+ adcode,
629
+ });
630
+ }
631
+ else {
632
+ resolve(null);
633
+ }
634
+ });
635
+ });
636
+ }
637
+ on(event, handler) {
638
+ if (!this.map)
639
+ return;
640
+ if (event === "click") {
641
+ this.map.on("click", (e) => {
642
+ const lng = e.lnglat.getLng();
643
+ const lat = e.lnglat.getLat();
644
+ handler(lng, lat);
645
+ });
646
+ }
647
+ else if (event === "movestart" || event === "moveend") {
648
+ // flying = true 期间(段 1 / 段 2 进行中)SDK 触发的 movestart / moveend
649
+ // 都被吞掉——上层 programmaticMoveRef 是单次消费设计,只期望一次 moveend。
650
+ // 段 3 启动前 flying 已被清为 false,段 3 的 movestart / moveend 正常上抛。
651
+ this.map.on(event, () => {
652
+ if (this.flying)
653
+ return;
654
+ handler();
655
+ });
656
+ }
657
+ }
658
+ }
659
+ exports.AMapProvider = AMapProvider;