clxx 3.0.2 → 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.
- package/AGENTS.md +49 -3
- package/build/Alert/style.js +15 -14
- package/build/CarouselNotice/style.js +2 -1
- package/build/CitySelect/style.js +40 -39
- package/build/Container/index.js +21 -1
- package/build/DatePicker/style.d.ts +1 -1
- package/build/DatePicker/style.js +25 -24
- package/build/Indicator/index.js +2 -1
- package/build/Loading/Wrapper.js +3 -2
- package/build/Loading/style.js +7 -6
- package/build/MapLocationSelection/buildSelectedLocation.d.ts +16 -0
- package/build/MapLocationSelection/buildSelectedLocation.js +123 -0
- package/build/MapLocationSelection/createProvider.d.ts +8 -0
- package/build/MapLocationSelection/createProvider.js +33 -0
- package/build/MapLocationSelection/getLocation.d.ts +8 -0
- package/build/MapLocationSelection/getLocation.js +112 -0
- package/build/MapLocationSelection/index.d.ts +16 -0
- package/build/MapLocationSelection/index.js +985 -0
- package/build/MapLocationSelection/loader.amap.d.ts +48 -0
- package/build/MapLocationSelection/loader.amap.js +125 -0
- package/build/MapLocationSelection/loader.bmap.d.ts +8 -0
- package/build/MapLocationSelection/loader.bmap.js +60 -0
- package/build/MapLocationSelection/provider.amap.d.ts +38 -0
- package/build/MapLocationSelection/provider.amap.js +659 -0
- package/build/MapLocationSelection/provider.bmap.d.ts +36 -0
- package/build/MapLocationSelection/provider.bmap.js +837 -0
- package/build/MapLocationSelection/provider.d.ts +45 -0
- package/build/MapLocationSelection/provider.js +10 -0
- package/build/MapLocationSelection/style.d.ts +4 -0
- package/build/MapLocationSelection/style.js +442 -0
- package/build/MapLocationSelection/types.d.ts +29 -0
- package/build/MapLocationSelection/types.js +22 -0
- package/build/MapLocationSelection/userMarker.d.ts +2 -0
- package/build/MapLocationSelection/userMarker.js +95 -0
- package/build/RegionPicker/style.js +34 -33
- package/build/ScrollView/style.js +5 -4
- package/build/Toast/style.js +6 -5
- package/build/index.d.ts +3 -0
- package/build/index.js +8 -2
- package/build/utils/rem.d.ts +1 -0
- package/build/utils/rem.js +48 -0
- package/package.json +2 -2
- package/test/src/index/index.jsx +5 -0
- package/test/src/index.jsx +1 -0
- package/test/src/map-location-selection/index.jsx +192 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Coord, POIItem } from "./types";
|
|
2
|
+
export interface MapProviderInitOptions {
|
|
3
|
+
container: HTMLElement;
|
|
4
|
+
initialCenter?: Coord;
|
|
5
|
+
initialZoom?: number;
|
|
6
|
+
initialCity?: string;
|
|
7
|
+
primary?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SearchOptions {
|
|
10
|
+
page?: number;
|
|
11
|
+
pageSize: number;
|
|
12
|
+
radius: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchAroundResult {
|
|
15
|
+
pois: POIItem[];
|
|
16
|
+
hasMore: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ReverseGeocodeResult {
|
|
19
|
+
name: string;
|
|
20
|
+
address: string;
|
|
21
|
+
province?: string;
|
|
22
|
+
city?: string;
|
|
23
|
+
district?: string;
|
|
24
|
+
adcode?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface GeolocateOptions {
|
|
27
|
+
allowIpFallback?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type MapProviderEvent = "movestart" | "moveend" | "click";
|
|
30
|
+
export interface MapProvider {
|
|
31
|
+
init(opts: MapProviderInitOptions): Promise<void>;
|
|
32
|
+
initHeadless(opts?: Pick<MapProviderInitOptions, "initialCity">): Promise<void>;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
getCenter(): Coord;
|
|
35
|
+
setCenter(center: Coord, zoom?: number): void;
|
|
36
|
+
upsertUserMarker(center: Coord): void;
|
|
37
|
+
searchAround(center: Coord, options: SearchOptions): Promise<SearchAroundResult>;
|
|
38
|
+
searchByKeyword(center: Coord, keyword: string, options: SearchOptions): Promise<POIItem[]>;
|
|
39
|
+
geolocate(options?: GeolocateOptions): Promise<Coord | null>;
|
|
40
|
+
reverseGeocode(center: Coord): Promise<ReverseGeocodeResult | null>;
|
|
41
|
+
on(event: "movestart", handler: () => void): void;
|
|
42
|
+
on(event: "moveend", handler: () => void): void;
|
|
43
|
+
on(event: "click", handler: (lng: number, lat: number) => void): void;
|
|
44
|
+
}
|
|
45
|
+
export type MapProviderType = "amap" | "bmap";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// MapProvider:把"具体地图 SDK"对 UI 层屏蔽掉的统一抽象。
|
|
3
|
+
//
|
|
4
|
+
// 设计原则:
|
|
5
|
+
// - UI 不知道地图实例的存在,只通过 provider 操作;
|
|
6
|
+
// - provider 内部坐标系自洽(amap=GCJ02、bmap=BD09),传入传出都是同一坐标系;
|
|
7
|
+
// - 事件订阅是"注册一次,永不取消"——和当前 UI 行为对齐,避免 provider 内部
|
|
8
|
+
// 管理 listener 列表的复杂度;
|
|
9
|
+
// - provider 在 init 之后才允许其它方法被调用,UI 层负责保证这个顺序。
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PRIMARY = void 0;
|
|
4
|
+
exports.createStyle = createStyle;
|
|
5
|
+
const react_1 = require("@emotion/react");
|
|
6
|
+
const color_1 = require("../utils/color");
|
|
7
|
+
const theme_1 = require("../utils/theme");
|
|
8
|
+
const rem_1 = require("../utils/rem");
|
|
9
|
+
// 设计变量(与 CitySelect 保持一致)
|
|
10
|
+
const textPrimary = "#1f2328";
|
|
11
|
+
const textSecondary = "#6b7280";
|
|
12
|
+
const textTertiary = "#9ca3af";
|
|
13
|
+
const bgPage = "#ffffff";
|
|
14
|
+
const bgSubtle = "#f5f6f8";
|
|
15
|
+
const border = "#e5e7eb";
|
|
16
|
+
exports.DEFAULT_PRIMARY = "#2f7dff";
|
|
17
|
+
// 浮层按钮基础样式(白底 + 柔和阴影,叠加在地图上保证可读,贴近 CitySelect 的扫平质感)
|
|
18
|
+
const flatBase = {
|
|
19
|
+
backgroundColor: bgPage,
|
|
20
|
+
color: textPrimary,
|
|
21
|
+
border: "none",
|
|
22
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.08), 0 0 1px rgba(0,0,0,0.06)",
|
|
23
|
+
};
|
|
24
|
+
function createStyle(primary) {
|
|
25
|
+
const primaryActive = (0, color_1.darken)(primary, 0.15);
|
|
26
|
+
// 抬起 / 回弹的几何换算:
|
|
27
|
+
// - pin 容器高 0.72rem,整体抬起幅度对应 pin 容器的 28%
|
|
28
|
+
// - head 高度占容器 60%,上抬 28%/60% ≈ 46.67% 自身高度
|
|
29
|
+
// - stem 高度占容器 37.5%,scaleY 起点 1,抬起后 (37.5%+28%)/37.5% ≈ 1.7467;
|
|
30
|
+
// stem 的 transform-origin 设在底部(即针尖处),保证 scaleY 改变时针尖不动
|
|
31
|
+
// 这样「抬起」只让针头浮起、针体拉长,针尖始终钉在屏幕中心 = 地图中心,
|
|
32
|
+
// 避免出现「拖动时看见的针尖指向点」与「松手后实际选中的中心点」错位。
|
|
33
|
+
//
|
|
34
|
+
// 弹簧关键帧(pin 高度相对幅度):抬起 +28% → -5% → +4% → -2% → +1% → 0
|
|
35
|
+
// 首次过冲从 4% 加大到 5%、衰减比按 ~1:0.8:0.5:0.2,更接近真实弹簧
|
|
36
|
+
// 时段分布前重后轻(28% / 48% / 65% / 82%),让"砸下→第一次回弹"最显著,
|
|
37
|
+
// 余下的小回弹快速收敛,配合 0.5s 总时长整体更轻快、更"脆"
|
|
38
|
+
const pinHeadDropKf = (0, react_1.keyframes)({
|
|
39
|
+
"0%": { transform: "translate(-50%, -46.67%)" },
|
|
40
|
+
"28%": { transform: "translate(-50%, 8.33%)" },
|
|
41
|
+
"48%": { transform: "translate(-50%, -6.67%)" },
|
|
42
|
+
"65%": { transform: "translate(-50%, 3.33%)" },
|
|
43
|
+
"82%": { transform: "translate(-50%, -1.67%)" },
|
|
44
|
+
"100%": { transform: "translate(-50%, 0%)" },
|
|
45
|
+
});
|
|
46
|
+
const pinStemDropKf = (0, react_1.keyframes)({
|
|
47
|
+
"0%": { transform: "translateX(-50%) scaleY(1.7467)" },
|
|
48
|
+
"28%": { transform: "translateX(-50%) scaleY(0.8667)" },
|
|
49
|
+
"48%": { transform: "translateX(-50%) scaleY(1.1067)" },
|
|
50
|
+
"65%": { transform: "translateX(-50%) scaleY(0.9467)" },
|
|
51
|
+
"82%": { transform: "translateX(-50%) scaleY(1.0267)" },
|
|
52
|
+
"100%": { transform: "translateX(-50%) scaleY(1)" },
|
|
53
|
+
});
|
|
54
|
+
// 定位中 spinner 的旋转动画
|
|
55
|
+
const spinKf = (0, react_1.keyframes)({
|
|
56
|
+
"0%": { transform: "rotate(0deg)" },
|
|
57
|
+
"100%": { transform: "rotate(360deg)" },
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
inner: (0, react_1.css)({
|
|
61
|
+
position: "relative",
|
|
62
|
+
width: "100%",
|
|
63
|
+
height: "100%",
|
|
64
|
+
backgroundColor: bgPage,
|
|
65
|
+
display: "flex",
|
|
66
|
+
flexDirection: "column",
|
|
67
|
+
color: textPrimary,
|
|
68
|
+
fontFamily: theme_1.fontStack,
|
|
69
|
+
WebkitFontSmoothing: "antialiased",
|
|
70
|
+
MozOsxFontSmoothing: "grayscale",
|
|
71
|
+
userSelect: "none",
|
|
72
|
+
overflow: "hidden",
|
|
73
|
+
}),
|
|
74
|
+
// 顶部地图区域
|
|
75
|
+
mapWrap: (0, react_1.css)({
|
|
76
|
+
position: "relative",
|
|
77
|
+
flex: "0 0 55%",
|
|
78
|
+
width: "100%",
|
|
79
|
+
backgroundColor: "#e8eaef",
|
|
80
|
+
overflow: "hidden",
|
|
81
|
+
}),
|
|
82
|
+
mapContainer: (0, react_1.css)({
|
|
83
|
+
width: "100%",
|
|
84
|
+
height: "100%",
|
|
85
|
+
// 必须显式 touch-action: none,让地图 SDK 接收完整的 touch 事件链
|
|
86
|
+
// (单指拖动、双指捏合缩放、双击放大),由 SDK 内部实现这些手势。
|
|
87
|
+
//
|
|
88
|
+
// 为什么必须显式设:clxx 的 Container 在 <html> 上设了
|
|
89
|
+
// touch-action: manipulation(= pan-x pan-y pinch-zoom),意思是
|
|
90
|
+
// "单指 pan 和双指 pinch-zoom 都交给浏览器处理"。touch-action 在事件
|
|
91
|
+
// 处理时是从触发元素到 root 逐级应用 OR 限制——任何祖先设了限制,
|
|
92
|
+
// 后代就受限,导致:
|
|
93
|
+
// * 移动端用户双指捏合地图,浏览器优先 capture 去尝试缩放整个页面
|
|
94
|
+
// (viewport 未禁用 user-scalable 时);
|
|
95
|
+
// * 地图 SDK 完全收不到 pinch 事件,**双指缩放失效**——典型现象就是
|
|
96
|
+
// "在地图上捏不动"。
|
|
97
|
+
// 子元素显式 touch-action: none 会覆盖祖先的 manipulation,把所有 touch
|
|
98
|
+
// 事件交回给元素自己处理。这是地图组件的标准做法。
|
|
99
|
+
touchAction: "none",
|
|
100
|
+
}),
|
|
101
|
+
// 居中、不动的定位 pin
|
|
102
|
+
// 针尖在 wrapper 内部 y=95.5% 处(与原 SVG viewBox 中针尖 y=977.92/1024 一致),
|
|
103
|
+
// 所以 translateY=-95.5% 才能让针尖与地图几何中心(用户当前位置 marker 中心)严格对齐
|
|
104
|
+
// 注意:抬起 / 落下动画已迁移到子元素 head / stem 上,容器位置永远固定,
|
|
105
|
+
// 这样针尖始终对准地图中心,避免「拖动时看到的针尖位置」与「松手后实际选中点」错位
|
|
106
|
+
centerPin: (0, react_1.css)({
|
|
107
|
+
position: "absolute",
|
|
108
|
+
top: "50%",
|
|
109
|
+
left: "50%",
|
|
110
|
+
transform: "translate(-50%, -95.5%)",
|
|
111
|
+
pointerEvents: "none",
|
|
112
|
+
width: (0, rem_1.r)(72),
|
|
113
|
+
height: (0, rem_1.r)(72),
|
|
114
|
+
zIndex: 5,
|
|
115
|
+
}),
|
|
116
|
+
// pin 头部圆环:原 SVG viewBox 中外径 60%、环厚约外径的 30%
|
|
117
|
+
// 以 border 模拟“甜甜圈”,中间镂空让针体在重叠区被环覆盖
|
|
118
|
+
// 加一层细投影提升悬浮感,使 pin 在地图底图上有"贴近但漂浮"的层次
|
|
119
|
+
centerPinHead: (0, react_1.css)({
|
|
120
|
+
position: "absolute",
|
|
121
|
+
top: 0,
|
|
122
|
+
left: "50%",
|
|
123
|
+
transform: "translateX(-50%)",
|
|
124
|
+
width: "60%",
|
|
125
|
+
height: "60%",
|
|
126
|
+
borderRadius: "50%",
|
|
127
|
+
border: `${(0, rem_1.r)(13)} solid ${primary}`,
|
|
128
|
+
boxSizing: "border-box",
|
|
129
|
+
boxShadow: "0 4px 10px rgba(0,0,0,0.18), 0 1px 2px rgba(0,0,0,0.08)",
|
|
130
|
+
transition: "transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
131
|
+
willChange: "transform",
|
|
132
|
+
}),
|
|
133
|
+
// 抬起:仅针头上移 46.67% 自身高度(= 28% pin 高度),针尖不动
|
|
134
|
+
centerPinHeadLifted: (0, react_1.css)({
|
|
135
|
+
transform: "translate(-50%, -46.67%)",
|
|
136
|
+
transition: "transform 0.16s ease-out",
|
|
137
|
+
}),
|
|
138
|
+
// 落下 + 多次回弹:与 stem 同步播放
|
|
139
|
+
centerPinHeadDrop: (0, react_1.css)({
|
|
140
|
+
animation: `${pinHeadDropKf} 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) 1`,
|
|
141
|
+
}),
|
|
142
|
+
// pin 针体:宽 5%、从 58% 处伸到 95.5% 处的细矩形,底部加轻微圆角对齐原 SVG 弧线
|
|
143
|
+
// 与头部保持 ~2% 重叠,源序在头部之前,确保头部在重叠区盖住针体
|
|
144
|
+
// transform-origin 设在底部(即针尖处),scaleY 改变时针尖始终钉在原位
|
|
145
|
+
centerPinStem: (0, react_1.css)({
|
|
146
|
+
position: "absolute",
|
|
147
|
+
top: "58%",
|
|
148
|
+
left: "50%",
|
|
149
|
+
transform: "translateX(-50%)",
|
|
150
|
+
transformOrigin: "50% 100%",
|
|
151
|
+
width: "5%",
|
|
152
|
+
height: "37.5%",
|
|
153
|
+
backgroundColor: "#5D5D5D",
|
|
154
|
+
borderBottomLeftRadius: "50% 8%",
|
|
155
|
+
borderBottomRightRadius: "50% 8%",
|
|
156
|
+
transition: "transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
157
|
+
willChange: "transform",
|
|
158
|
+
}),
|
|
159
|
+
// 抬起:针体被纵向拉长到 1.7467 倍(= (37.5%+28%) / 37.5%),针尖端不动
|
|
160
|
+
centerPinStemLifted: (0, react_1.css)({
|
|
161
|
+
transform: "translateX(-50%) scaleY(1.7467)",
|
|
162
|
+
transition: "transform 0.16s ease-out",
|
|
163
|
+
}),
|
|
164
|
+
// 落下 + 多次回弹:与 head 同步播放
|
|
165
|
+
centerPinStemDrop: (0, react_1.css)({
|
|
166
|
+
animation: `${pinStemDropKf} 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) 1`,
|
|
167
|
+
}),
|
|
168
|
+
// 顶部一行:取消(左) / 确定(右)
|
|
169
|
+
topBar: (0, react_1.css)({
|
|
170
|
+
position: "absolute",
|
|
171
|
+
top: 0,
|
|
172
|
+
left: 0,
|
|
173
|
+
right: 0,
|
|
174
|
+
padding: `calc(env(safe-area-inset-top, 0px) + ${(0, rem_1.r)(28)}) ${(0, rem_1.r)(28)} ${(0, rem_1.r)(16)}`,
|
|
175
|
+
display: "flex",
|
|
176
|
+
justifyContent: "space-between",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
gap: (0, rem_1.r)(16),
|
|
179
|
+
zIndex: 6,
|
|
180
|
+
pointerEvents: "none",
|
|
181
|
+
}),
|
|
182
|
+
// 取消:圆形图标按钮,配 CSS chevron-left;与 locateBtn 共享视觉语言
|
|
183
|
+
cancelBtn: (0, react_1.css)(Object.assign(Object.assign({}, flatBase), { pointerEvents: "auto", width: (0, rem_1.r)(54), height: (0, rem_1.r)(54), borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", transition: "background-color .12s, transform .12s", "&:active": {
|
|
184
|
+
backgroundColor: bgSubtle,
|
|
185
|
+
transform: "scale(0.94)",
|
|
186
|
+
} })),
|
|
187
|
+
// 纯 CSS 画 chevron-left:两条相邻 border 旋转 -45°,避免再引一段 SVG path
|
|
188
|
+
cancelBtnIcon: (0, react_1.css)({
|
|
189
|
+
width: (0, rem_1.r)(18),
|
|
190
|
+
height: (0, rem_1.r)(18),
|
|
191
|
+
borderTop: `${(0, rem_1.r)(5)} solid ${textPrimary}`,
|
|
192
|
+
borderLeft: `${(0, rem_1.r)(5)} solid ${textPrimary}`,
|
|
193
|
+
borderRadius: (0, rem_1.r)(2.5),
|
|
194
|
+
transform: "rotate(-45deg)",
|
|
195
|
+
// 视觉重心补偿:chevron 旋转后偏右,左移让光学上更居中
|
|
196
|
+
marginLeft: (0, rem_1.r)(4),
|
|
197
|
+
}),
|
|
198
|
+
// 确定:更醒目的主行动按钮,主色调阴影 + 按压缩放
|
|
199
|
+
confirmBtn: (0, react_1.css)({
|
|
200
|
+
pointerEvents: "auto",
|
|
201
|
+
minWidth: (0, rem_1.r)(88),
|
|
202
|
+
height: (0, rem_1.r)(54),
|
|
203
|
+
lineHeight: (0, rem_1.r)(54),
|
|
204
|
+
padding: `0 ${(0, rem_1.r)(24)}`,
|
|
205
|
+
backgroundColor: primary,
|
|
206
|
+
color: "#fff",
|
|
207
|
+
fontSize: (0, rem_1.r)(24),
|
|
208
|
+
fontWeight: 600,
|
|
209
|
+
letterSpacing: (0, rem_1.r)(2),
|
|
210
|
+
borderRadius: (0, rem_1.r)(27),
|
|
211
|
+
textAlign: "center",
|
|
212
|
+
border: "none",
|
|
213
|
+
// 主色 ~31% 透明的彩色阴影 + 1px 描边阴影,提升「浮起」与精致感
|
|
214
|
+
// boxShadow: `0 5px 14px ${primary}50, 0 1px 2px rgba(0,0,0,0.08)`,
|
|
215
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.08), 0 0 1px rgba(0,0,0,0.06)",
|
|
216
|
+
transition: "background-color .12s, transform .12s, box-shadow .12s",
|
|
217
|
+
"&:active": {
|
|
218
|
+
backgroundColor: primaryActive,
|
|
219
|
+
transform: "scale(0.96)",
|
|
220
|
+
boxShadow: `0 3px 9px ${primary}40, 0 1px 2px rgba(0,0,0,0.08)`,
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
confirmDisabled: (0, react_1.css)({
|
|
224
|
+
opacity: 0.55,
|
|
225
|
+
pointerEvents: "none",
|
|
226
|
+
}),
|
|
227
|
+
// 「回到当前位置」按钮:与 topBar 右边距对齐 (0.28rem),上抬避开高德版权
|
|
228
|
+
locateBtn: (0, react_1.css)(Object.assign(Object.assign({}, flatBase), { position: "absolute", right: (0, rem_1.r)(28), bottom: (0, rem_1.r)(40), zIndex: 6, width: (0, rem_1.r)(60), height: (0, rem_1.r)(60), borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", transition: "background-color .12s, transform .12s", "&:active": {
|
|
229
|
+
backgroundColor: bgSubtle,
|
|
230
|
+
transform: "scale(0.94)",
|
|
231
|
+
} })),
|
|
232
|
+
locateBtnIcon: (0, react_1.css)({
|
|
233
|
+
width: (0, rem_1.r)(30),
|
|
234
|
+
height: (0, rem_1.r)(30),
|
|
235
|
+
fill: textPrimary,
|
|
236
|
+
transition: "fill .12s",
|
|
237
|
+
}),
|
|
238
|
+
// 按钮内置 loading spinner:仅在 locateBtn 自身位置打转,不再用整屏 mask
|
|
239
|
+
locateBtnSpinner: (0, react_1.css)({
|
|
240
|
+
width: (0, rem_1.r)(32),
|
|
241
|
+
height: (0, rem_1.r)(32),
|
|
242
|
+
borderRadius: "50%",
|
|
243
|
+
border: `${(0, rem_1.r)(4)} solid rgba(0,0,0,0.12)`,
|
|
244
|
+
borderTopColor: primary,
|
|
245
|
+
animation: `${spinKf} 0.7s linear infinite`,
|
|
246
|
+
boxSizing: "border-box",
|
|
247
|
+
}),
|
|
248
|
+
// 下半部分容器
|
|
249
|
+
bottom: (0, react_1.css)({
|
|
250
|
+
flex: 1,
|
|
251
|
+
minHeight: 0,
|
|
252
|
+
display: "flex",
|
|
253
|
+
flexDirection: "column",
|
|
254
|
+
backgroundColor: bgPage,
|
|
255
|
+
}),
|
|
256
|
+
// 搜索区域:贴在下半屏顶部,与 listArea 垂直堆叠。
|
|
257
|
+
// 视觉规格沿用历史版本:白底卡片 + 圆角灰底输入条 + 左侧放大镜 + 右侧清除 X。
|
|
258
|
+
// 之所以让搜索框独立于地图浮层(而非顶部 topBar),是因为打车场景下用户多数情况
|
|
259
|
+
// 是先拖图选位置,搜索仅作为兜底入口;放在列表上方更符合视觉重心。
|
|
260
|
+
searchBox: (0, react_1.css)({
|
|
261
|
+
padding: `${(0, rem_1.r)(24)} ${(0, rem_1.r)(30)}`,
|
|
262
|
+
borderBottom: `1px solid ${border}`,
|
|
263
|
+
}),
|
|
264
|
+
searchInner: (0, react_1.css)({
|
|
265
|
+
display: "flex",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
height: (0, rem_1.r)(72),
|
|
268
|
+
backgroundColor: bgSubtle,
|
|
269
|
+
borderRadius: (0, rem_1.r)(36),
|
|
270
|
+
padding: `0 ${(0, rem_1.r)(24)}`,
|
|
271
|
+
}),
|
|
272
|
+
searchIcon: (0, react_1.css)({
|
|
273
|
+
width: (0, rem_1.r)(32),
|
|
274
|
+
height: (0, rem_1.r)(32),
|
|
275
|
+
fill: textTertiary,
|
|
276
|
+
marginRight: (0, rem_1.r)(15),
|
|
277
|
+
flexShrink: 0,
|
|
278
|
+
}),
|
|
279
|
+
searchInput: (0, react_1.css)({
|
|
280
|
+
flex: 1,
|
|
281
|
+
minWidth: 0,
|
|
282
|
+
height: "100%",
|
|
283
|
+
fontSize: (0, rem_1.r)(28),
|
|
284
|
+
lineHeight: (0, rem_1.r)(72),
|
|
285
|
+
color: textPrimary,
|
|
286
|
+
fontFamily: "inherit",
|
|
287
|
+
border: "none",
|
|
288
|
+
outline: "none",
|
|
289
|
+
backgroundColor: "transparent",
|
|
290
|
+
padding: 0,
|
|
291
|
+
"&::placeholder": {
|
|
292
|
+
color: textTertiary,
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
searchClear: (0, react_1.css)({
|
|
296
|
+
width: (0, rem_1.r)(34),
|
|
297
|
+
height: (0, rem_1.r)(34),
|
|
298
|
+
display: "flex",
|
|
299
|
+
alignItems: "center",
|
|
300
|
+
justifyContent: "center",
|
|
301
|
+
flexShrink: 0,
|
|
302
|
+
cursor: "pointer",
|
|
303
|
+
}),
|
|
304
|
+
searchClearIcon: (0, react_1.css)({
|
|
305
|
+
width: (0, rem_1.r)(34),
|
|
306
|
+
height: (0, rem_1.r)(34),
|
|
307
|
+
display: "block",
|
|
308
|
+
fill: textTertiary,
|
|
309
|
+
}),
|
|
310
|
+
// 定位中:搜索框降色阶(输入框本身已 disabled,这里让外观也对齐)
|
|
311
|
+
searchDisabled: (0, react_1.css)({
|
|
312
|
+
opacity: 0.55,
|
|
313
|
+
}),
|
|
314
|
+
// 列表区域容器(高度 = bottom 剩余空间)
|
|
315
|
+
// position:relative 让 listLoadingMask 能用 absolute inset:0 精确盖住列表
|
|
316
|
+
listArea: (0, react_1.css)({
|
|
317
|
+
position: "relative",
|
|
318
|
+
flex: 1,
|
|
319
|
+
minHeight: 0,
|
|
320
|
+
}),
|
|
321
|
+
item: (0, react_1.css)({
|
|
322
|
+
position: "relative",
|
|
323
|
+
display: "flex",
|
|
324
|
+
alignItems: "flex-start",
|
|
325
|
+
padding: `${(0, rem_1.r)(24)} ${(0, rem_1.r)(30)}`,
|
|
326
|
+
borderBottom: `1px solid ${border}`,
|
|
327
|
+
backgroundColor: bgPage,
|
|
328
|
+
transition: "background-color .12s",
|
|
329
|
+
"&:active": {
|
|
330
|
+
backgroundColor: bgSubtle,
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
itemBody: (0, react_1.css)({
|
|
334
|
+
flex: 1,
|
|
335
|
+
minWidth: 0,
|
|
336
|
+
}),
|
|
337
|
+
itemTitle: (0, react_1.css)({
|
|
338
|
+
fontSize: (0, rem_1.r)(30),
|
|
339
|
+
color: textPrimary,
|
|
340
|
+
lineHeight: 1.4,
|
|
341
|
+
overflow: "hidden",
|
|
342
|
+
textOverflow: "ellipsis",
|
|
343
|
+
whiteSpace: "nowrap",
|
|
344
|
+
}),
|
|
345
|
+
itemDesc: (0, react_1.css)({
|
|
346
|
+
marginTop: (0, rem_1.r)(6),
|
|
347
|
+
fontSize: (0, rem_1.r)(22),
|
|
348
|
+
color: textSecondary,
|
|
349
|
+
lineHeight: 1.4,
|
|
350
|
+
overflow: "hidden",
|
|
351
|
+
textOverflow: "ellipsis",
|
|
352
|
+
whiteSpace: "nowrap",
|
|
353
|
+
}),
|
|
354
|
+
// 选中态:标题用主色 + 加粗,跟右侧 itemCheck 钩选呼应。
|
|
355
|
+
itemTitleActive: (0, react_1.css)({
|
|
356
|
+
color: primary,
|
|
357
|
+
fontWeight: 500,
|
|
358
|
+
}),
|
|
359
|
+
itemCheck: (0, react_1.css)({
|
|
360
|
+
width: (0, rem_1.r)(50),
|
|
361
|
+
height: (0, rem_1.r)(50),
|
|
362
|
+
marginLeft: (0, rem_1.r)(20),
|
|
363
|
+
flexShrink: 0,
|
|
364
|
+
fill: primary,
|
|
365
|
+
alignSelf: "center",
|
|
366
|
+
}),
|
|
367
|
+
empty: (0, react_1.css)({
|
|
368
|
+
padding: `${(0, rem_1.r)(60)} 0`,
|
|
369
|
+
display: "flex",
|
|
370
|
+
alignItems: "center",
|
|
371
|
+
justifyContent: "center",
|
|
372
|
+
color: textTertiary,
|
|
373
|
+
fontSize: (0, rem_1.r)(26),
|
|
374
|
+
}),
|
|
375
|
+
listEnd: (0, react_1.css)({
|
|
376
|
+
padding: `${(0, rem_1.r)(24)} 0`,
|
|
377
|
+
textAlign: "center",
|
|
378
|
+
color: textTertiary,
|
|
379
|
+
fontSize: (0, rem_1.r)(22),
|
|
380
|
+
}),
|
|
381
|
+
// 定位中:地图层 —— 半透明白底 + 更柔的玻璃模糊 + 居中 spinner + 小号文字
|
|
382
|
+
locatingMask: (0, react_1.css)({
|
|
383
|
+
position: "absolute",
|
|
384
|
+
inset: 0,
|
|
385
|
+
backgroundColor: "rgba(255,255,255,0.62)",
|
|
386
|
+
WebkitBackdropFilter: "blur(3px)",
|
|
387
|
+
backdropFilter: "blur(3px)",
|
|
388
|
+
display: "flex",
|
|
389
|
+
flexDirection: "column",
|
|
390
|
+
alignItems: "center",
|
|
391
|
+
justifyContent: "center",
|
|
392
|
+
gap: (0, rem_1.r)(18),
|
|
393
|
+
color: textPrimary,
|
|
394
|
+
fontSize: (0, rem_1.r)(24),
|
|
395
|
+
fontWeight: 500,
|
|
396
|
+
letterSpacing: (0, rem_1.r)(4),
|
|
397
|
+
zIndex: 8,
|
|
398
|
+
}),
|
|
399
|
+
// 旋转的 spinner 圆环
|
|
400
|
+
spinner: (0, react_1.css)({
|
|
401
|
+
width: (0, rem_1.r)(50),
|
|
402
|
+
height: (0, rem_1.r)(50),
|
|
403
|
+
borderRadius: "50%",
|
|
404
|
+
border: `${(0, rem_1.r)(5)} solid rgba(0,0,0,0.08)`,
|
|
405
|
+
borderTopColor: primary,
|
|
406
|
+
animation: `${spinKf} 0.8s linear infinite`,
|
|
407
|
+
boxSizing: "border-box",
|
|
408
|
+
}),
|
|
409
|
+
// 定位中:底部搜索 + 列表的锁定遮罩(透明,仅拦截点击)
|
|
410
|
+
bottomLockedMask: (0, react_1.css)({
|
|
411
|
+
position: "absolute",
|
|
412
|
+
inset: 0,
|
|
413
|
+
backgroundColor: "rgba(255,255,255,0.45)",
|
|
414
|
+
zIndex: 9,
|
|
415
|
+
cursor: "not-allowed",
|
|
416
|
+
}),
|
|
417
|
+
// 列表加载中:仅盖在 listArea 上的半透明遮罩 + 居中 spinner。
|
|
418
|
+
//
|
|
419
|
+
// 与 bottomLockedMask 的区别:
|
|
420
|
+
// - bottomLockedMask 盖整个 bottom 区域(含搜索框),只在首次定位中触发;
|
|
421
|
+
// - listLoadingMask 仅盖列表区,搜索框保持可输入——keyword 搜索期间用户可
|
|
422
|
+
// 继续打字修改关键字,不应被列表 loading 遮罩拦住交互。
|
|
423
|
+
//
|
|
424
|
+
// zIndex=4 比 ScrollView 高、比 bottomLockedMask(9) / locatingMask(8) 低——
|
|
425
|
+
// 这样首次定位中的全屏遮罩仍能盖在列表 loading 之上(避免视觉双层叠加)。
|
|
426
|
+
listLoadingMask: (0, react_1.css)({
|
|
427
|
+
position: "absolute",
|
|
428
|
+
inset: 0,
|
|
429
|
+
backgroundColor: "rgba(255,255,255,0.62)",
|
|
430
|
+
WebkitBackdropFilter: "blur(2px)",
|
|
431
|
+
backdropFilter: "blur(2px)",
|
|
432
|
+
display: "flex",
|
|
433
|
+
alignItems: "center",
|
|
434
|
+
justifyContent: "center",
|
|
435
|
+
zIndex: 4,
|
|
436
|
+
}),
|
|
437
|
+
// 底部容器需要相对定位,遮罩才能盖住
|
|
438
|
+
bottomRelative: (0, react_1.css)({
|
|
439
|
+
position: "relative",
|
|
440
|
+
}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Coord = [number, number];
|
|
2
|
+
export interface POIItem {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
address: string;
|
|
6
|
+
location: {
|
|
7
|
+
lng: number;
|
|
8
|
+
lat: number;
|
|
9
|
+
};
|
|
10
|
+
cityname?: string;
|
|
11
|
+
pname?: string;
|
|
12
|
+
adname?: string;
|
|
13
|
+
distance?: number;
|
|
14
|
+
raw: any;
|
|
15
|
+
}
|
|
16
|
+
export interface SelectedLocation {
|
|
17
|
+
name: string;
|
|
18
|
+
address: string;
|
|
19
|
+
longitude: number;
|
|
20
|
+
latitude: number;
|
|
21
|
+
city?: string;
|
|
22
|
+
province?: string;
|
|
23
|
+
district?: string;
|
|
24
|
+
provinceCode?: string;
|
|
25
|
+
cityCode?: string;
|
|
26
|
+
districtCode?: string;
|
|
27
|
+
raw?: unknown;
|
|
28
|
+
}
|
|
29
|
+
export declare function haversineMeters(lng1: number, lat1: number, lng2: number, lat2: number): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ===== 与具体地图 SDK 解耦的共享类型 =====
|
|
3
|
+
//
|
|
4
|
+
// 设计原则:
|
|
5
|
+
// - UI 层(index.tsx)只看到这里的类型,不直接依赖 AMap / BMapGL;
|
|
6
|
+
// - 各 provider 内部把 SDK 原始数据映射成 POIItem 后再返回;
|
|
7
|
+
// - Coord 不约束坐标系:高德 provider 内部用 GCJ02,百度 provider 内部用 BD09,
|
|
8
|
+
// 两者只与「自己」交互,UI 把数字透传给业务方即可。
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.haversineMeters = haversineMeters;
|
|
11
|
+
// 球面距离(米)。两点都是同一坐标系下的经纬度即可——
|
|
12
|
+
// BD09 / GCJ02 / WGS84 在小尺度(< 数十公里)下半径误差可忽略。
|
|
13
|
+
function haversineMeters(lng1, lat1, lng2, lat2) {
|
|
14
|
+
const R = 6371000;
|
|
15
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
16
|
+
const dLat = toRad(lat2 - lat1);
|
|
17
|
+
const dLng = toRad(lng2 - lng1);
|
|
18
|
+
const a = Math.pow(Math.sin(dLat / 2), 2) +
|
|
19
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.pow(Math.sin(dLng / 2), 2);
|
|
20
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
21
|
+
return R * c;
|
|
22
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 用户当前位置 marker 的样式与 DOM 创建。
|
|
3
|
+
//
|
|
4
|
+
// === 中心对齐策略 ===
|
|
5
|
+
// 高德 AMap.Marker(content) 与百度 BMapGL.CustomOverlay 都允许塞任意 DOM 进来,
|
|
6
|
+
// 但两端的"DOM 锚点"机制完全不同——而且 BMapGL 在某些版本下 anchors / offsetX / offsetY
|
|
7
|
+
// 字段会被静默忽略(实测传 anchors=[0,0] 与 offsetY=6 都不生效),无法靠 SDK 参数把
|
|
8
|
+
// 12x12 的圆点中心对齐 point。
|
|
9
|
+
//
|
|
10
|
+
// 这里改用一个 SDK-无关的 trick:
|
|
11
|
+
// - 外层 wrap 是 0x0 的"锚点容器"。任何 SDK 算锚点 / 偏移时,
|
|
12
|
+
// 由于 width=height=0,所有候选锚点(top-left / 底部中心 / 居中 / ...)都退化为同一点
|
|
13
|
+
// —— 即 wrap 自身的左上角,也就是 point 在屏幕上的位置。
|
|
14
|
+
// - 内层 .mls-user-loc(12x12)用 absolute + (-6, -6) 让自己几何中心对齐 wrap(0, 0)。
|
|
15
|
+
//
|
|
16
|
+
// 因此只要给 SDK 传"原始 point"、不传任何 offset / anchor,圆点中心就一定落在 point 上,
|
|
17
|
+
// 无需关心 SDK 实际默认锚点是 top-left 还是底部居中、字段名是否被识别。
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.ensureUserLocStyle = ensureUserLocStyle;
|
|
20
|
+
exports.createUserMarkerDom = createUserMarkerDom;
|
|
21
|
+
const USER_LOC_STYLE_ID = "mls-user-loc-style";
|
|
22
|
+
function ensureUserLocStyle() {
|
|
23
|
+
if (typeof document === "undefined")
|
|
24
|
+
return;
|
|
25
|
+
if (document.getElementById(USER_LOC_STYLE_ID))
|
|
26
|
+
return;
|
|
27
|
+
const styleEl = document.createElement("style");
|
|
28
|
+
styleEl.id = USER_LOC_STYLE_ID;
|
|
29
|
+
// 仅保留 dot / ripple 两个子元素的视觉样式:尺寸 / 位置 / 涟漪动画。
|
|
30
|
+
// wrap 与 .mls-user-loc 的几何(width/height/position/left/top)全部由 createUserMarkerDom
|
|
31
|
+
// 在创建 DOM 时 inline 写死,这里不再声明,避免外部 CSS 加载顺序导致几何抖动。
|
|
32
|
+
styleEl.textContent = `
|
|
33
|
+
.mls-user-loc__dot {
|
|
34
|
+
position: absolute;
|
|
35
|
+
left: 50%;
|
|
36
|
+
top: 50%;
|
|
37
|
+
width: 9px;
|
|
38
|
+
height: 9px;
|
|
39
|
+
margin-left: -4.5px;
|
|
40
|
+
margin-top: -4.5px;
|
|
41
|
+
border-radius: 50%;
|
|
42
|
+
background-color: #4575F6;
|
|
43
|
+
box-shadow: 0 0 0 1.5px #ffffff, 0 1px 2px rgba(0,0,0,0.25);
|
|
44
|
+
}
|
|
45
|
+
.mls-user-loc__ripple {
|
|
46
|
+
position: absolute;
|
|
47
|
+
left: 50%;
|
|
48
|
+
top: 50%;
|
|
49
|
+
width: 9px;
|
|
50
|
+
height: 9px;
|
|
51
|
+
margin-left: -4.5px;
|
|
52
|
+
margin-top: -4.5px;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
background-color: #4575F6;
|
|
55
|
+
opacity: 0.5;
|
|
56
|
+
animation: mlsUserLocRipple 1.8s ease-out infinite;
|
|
57
|
+
}
|
|
58
|
+
@keyframes mlsUserLocRipple {
|
|
59
|
+
0% { transform: scale(1); opacity: 0.5; }
|
|
60
|
+
80% { transform: scale(3.5); opacity: 0; }
|
|
61
|
+
100% { transform: scale(3.5); opacity: 0; }
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
document.head.appendChild(styleEl);
|
|
65
|
+
}
|
|
66
|
+
function createUserMarkerDom() {
|
|
67
|
+
ensureUserLocStyle();
|
|
68
|
+
// wrap:0x0 锚点容器。所有几何相关字段都用 inline style 强制锁定,
|
|
69
|
+
// 完全不依赖外部 .mls-user-loc-wrap 类(避免 CSS 加载顺序、SDK 内部覆写带来的干扰)。
|
|
70
|
+
// SDK 内部可能把 position 改成 absolute——那也是 positioned,子元素 absolute 仍以
|
|
71
|
+
// wrap 为参考;transform 也可能被 SDK 设置——不影响 wrap 自身 0x0 的 layout 尺寸。
|
|
72
|
+
const wrap = document.createElement("div");
|
|
73
|
+
wrap.style.position = "relative";
|
|
74
|
+
wrap.style.width = "0";
|
|
75
|
+
wrap.style.height = "0";
|
|
76
|
+
wrap.style.padding = "0";
|
|
77
|
+
wrap.style.margin = "0";
|
|
78
|
+
wrap.style.border = "0";
|
|
79
|
+
wrap.style.boxSizing = "content-box";
|
|
80
|
+
wrap.style.pointerEvents = "none";
|
|
81
|
+
// 真实可见的 marker:absolute -6/-6 让自身几何中心对齐 wrap(0, 0)
|
|
82
|
+
const inner = document.createElement("div");
|
|
83
|
+
inner.className = "mls-user-loc";
|
|
84
|
+
inner.style.position = "absolute";
|
|
85
|
+
inner.style.left = "-6px";
|
|
86
|
+
inner.style.top = "-6px";
|
|
87
|
+
inner.style.width = "12px";
|
|
88
|
+
inner.style.height = "12px";
|
|
89
|
+
inner.style.pointerEvents = "none";
|
|
90
|
+
inner.innerHTML =
|
|
91
|
+
'<span class="mls-user-loc__ripple"></span>' +
|
|
92
|
+
'<span class="mls-user-loc__dot"></span>';
|
|
93
|
+
wrap.appendChild(inner);
|
|
94
|
+
return wrap;
|
|
95
|
+
}
|