clxx 2.1.6 → 2.1.8
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 +2 -0
- package/README.md +63 -0
- package/build/Ago/index.js +14 -8
- package/build/Alert/Wrapper.js +20 -20
- package/build/Alert/index.js +17 -11
- package/build/Alert/style.js +44 -26
- package/build/AutoGrid/index.js +50 -14
- package/build/AutoGrid/style.js +9 -6
- package/build/CarouselNotice/index.js +22 -19
- package/build/CarouselNotice/style.js +6 -3
- package/build/CitySelect/data.d.ts +3 -0
- package/build/CitySelect/data.js +2355 -0
- package/build/CitySelect/index.d.ts +17 -0
- package/build/CitySelect/index.js +335 -0
- package/build/CitySelect/search.d.ts +2 -0
- package/build/CitySelect/search.js +70 -0
- package/build/CitySelect/style.d.ts +4 -0
- package/build/CitySelect/style.js +237 -0
- package/build/CitySelect/type.d.ts +17 -0
- package/build/CitySelect/type.js +2 -0
- package/build/Clickable/index.js +56 -20
- package/build/Container/index.js +52 -16
- package/build/Countdowner/index.js +50 -14
- package/build/Dialog/Wrapper.js +13 -10
- package/build/Dialog/index.js +18 -12
- package/build/Dialog/style.js +29 -25
- package/build/Effect/useInterval.js +7 -4
- package/build/Effect/useTick.js +9 -6
- package/build/Effect/useUpdate.js +6 -3
- package/build/Effect/useViewport.js +15 -8
- package/build/Effect/useWindowResize.js +7 -4
- package/build/Fixed/index.js +6 -3
- package/build/Flex/Col.js +23 -15
- package/build/Flex/Row.js +23 -15
- package/build/Flex/index.js +9 -5
- package/build/Indicator/index.js +20 -14
- package/build/Indicator/style.js +6 -3
- package/build/Loading/Wrapper.js +14 -11
- package/build/Loading/index.js +17 -10
- package/build/Loading/style.js +9 -6
- package/build/Overlay/index.js +12 -9
- package/build/SafeArea/index.js +8 -5
- package/build/ScrollView/index.js +24 -21
- package/build/ScrollView/style.js +4 -1
- package/build/Toast/Toast.js +53 -17
- package/build/Toast/index.js +21 -14
- package/build/Toast/style.d.ts +0 -3
- package/build/Toast/style.js +49 -41
- package/build/index.d.ts +1 -1
- package/build/index.js +102 -36
- package/build/utils/Countdown.js +7 -3
- package/build/utils/ago.js +10 -4
- package/build/utils/calendarTable.js +9 -3
- package/build/utils/createApp.js +35 -27
- package/build/utils/cssUtil.js +10 -5
- package/build/utils/defaultScroll.js +4 -1
- package/build/utils/dom.js +6 -3
- package/build/utils/is.js +6 -2
- package/build/utils/jsonp.js +4 -1
- package/build/utils/request.js +40 -27
- package/build/utils/tick.js +4 -1
- package/build/utils/uniqKey.js +4 -1
- package/build/utils/wait.js +8 -4
- package/package.json +1 -1
- package/test/src/city-select/index.jsx +21 -0
- package/test/src/index/index.jsx +1 -0
- package/test/src/toast/index.jsx +1 -0
- package/test/vite.config.js +6 -2
- package/build/context.d.ts +0 -14
- package/build/context.js +0 -21
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SelectedCity {
|
|
2
|
+
name: string;
|
|
3
|
+
code: string;
|
|
4
|
+
province: {
|
|
5
|
+
name: string;
|
|
6
|
+
code: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface CitySelectProps {
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
onSelect?: (city: SelectedCity) => void;
|
|
12
|
+
onLetterChange?: (letter: string) => void;
|
|
13
|
+
getLocation?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
14
|
+
primary?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function CitySelect(props: CitySelectProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
17
|
+
export declare function showCitySelect(options?: Pick<CitySelectProps, "onSelect" | "onLetterChange" | "getLocation" | "primary">): void;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CitySelect = CitySelect;
|
|
4
|
+
exports.showCitySelect = showCitySelect;
|
|
5
|
+
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const dom_1 = require("../utils/dom");
|
|
8
|
+
const data_1 = require("./data");
|
|
9
|
+
const search_1 = require("./search");
|
|
10
|
+
const style_1 = require("./style");
|
|
11
|
+
const Col_1 = require("../Flex/Col");
|
|
12
|
+
const Row_1 = require("../Flex/Row");
|
|
13
|
+
const Clickable_1 = require("../Clickable");
|
|
14
|
+
// 在 cityData 中按 code 或 name 查找城市
|
|
15
|
+
// name 匹配时允许省略末尾的"市",如"北京" 匹配 "北京市"
|
|
16
|
+
function findCity(key) {
|
|
17
|
+
if (!key)
|
|
18
|
+
return null;
|
|
19
|
+
const k = key.trim();
|
|
20
|
+
if (!k)
|
|
21
|
+
return null;
|
|
22
|
+
const kNoCity = k.endsWith("市") ? k.slice(0, -1) : k;
|
|
23
|
+
for (const letter of Object.keys(data_1.cityData)) {
|
|
24
|
+
for (const item of data_1.cityData[letter]) {
|
|
25
|
+
if (item.code === k)
|
|
26
|
+
return item;
|
|
27
|
+
const nameNoCity = item.name.endsWith("市")
|
|
28
|
+
? item.name.slice(0, -1)
|
|
29
|
+
: item.name;
|
|
30
|
+
if (nameNoCity === kNoCity)
|
|
31
|
+
return item;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// 滑入/滑出动画在 style.inner 的 transition 中定义
|
|
37
|
+
function CitySelect(props) {
|
|
38
|
+
const { onClose, onSelect, onLetterChange, getLocation, primary = style_1.DEFAULT_PRIMARY, } = props;
|
|
39
|
+
const style = (0, react_1.useMemo)(() => (0, style_1.createStyle)(primary), [primary]);
|
|
40
|
+
const letters = (0, react_1.useMemo)(() => Object.keys(data_1.cityData), []);
|
|
41
|
+
// 定位解析结果:null 表示尚未解析或失败/未匹配;非空时展示快捷入口
|
|
42
|
+
const [locatedCity, setLocatedCity] = (0, react_1.useState)(null);
|
|
43
|
+
// 首次挂载时调用一次外部定位;异步失败或匹配不到均保持不展示
|
|
44
|
+
const getLocationRef = (0, react_1.useRef)(getLocation);
|
|
45
|
+
getLocationRef.current = getLocation;
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
const fn = getLocationRef.current;
|
|
48
|
+
if (!fn)
|
|
49
|
+
return;
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
try {
|
|
52
|
+
Promise.resolve(fn())
|
|
53
|
+
.then((key) => {
|
|
54
|
+
if (cancelled)
|
|
55
|
+
return;
|
|
56
|
+
const city = findCity(key !== null && key !== void 0 ? key : undefined);
|
|
57
|
+
if (city)
|
|
58
|
+
setLocatedCity(city);
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
// 定位失败:保持不展示
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (_a) {
|
|
65
|
+
// 同步抛错:保持不展示
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
const sidebarRef = (0, react_1.useRef)(null);
|
|
72
|
+
const listRef = (0, react_1.useRef)(null);
|
|
73
|
+
const sectionRefs = (0, react_1.useRef)({});
|
|
74
|
+
// 缓存每个字母区块在列表内的 offsetTop,避免每次 scroll 都 layout
|
|
75
|
+
const sectionOffsetsRef = (0, react_1.useRef)([]);
|
|
76
|
+
// 触摸期间抽帧 set scrollTop,标记忽略 scroll 回调,避免抗抽
|
|
77
|
+
const isTouchScrollRef = (0, react_1.useRef)(false);
|
|
78
|
+
const [activeLetter, setActiveLetter] = (0, react_1.useState)(() => { var _a; return (_a = letters[0]) !== null && _a !== void 0 ? _a : null; });
|
|
79
|
+
const [touching, setTouching] = (0, react_1.useState)(false);
|
|
80
|
+
const [keyword, setKeyword] = (0, react_1.useState)("");
|
|
81
|
+
const [composing, setComposing] = (0, react_1.useState)(false);
|
|
82
|
+
// 动画状态:entering 刚挂载还未滑入;active 已滑入到位;exiting 正在滑出
|
|
83
|
+
const [phase, setPhase] = (0, react_1.useState)("entering");
|
|
84
|
+
// IME 合成期间不触发搜索,避免中文输入时的拼音中间态干扰结果
|
|
85
|
+
const searchResult = (0, react_1.useMemo)(() => {
|
|
86
|
+
if (composing)
|
|
87
|
+
return null;
|
|
88
|
+
return keyword.trim() ? (0, search_1.searchCity)(keyword) : null;
|
|
89
|
+
}, [keyword, composing]);
|
|
90
|
+
const isSearching = searchResult !== null;
|
|
91
|
+
// 用 ref 保存最新值,避免 effect 依赖变化而反复解绑/绑定
|
|
92
|
+
const lettersRef = (0, react_1.useRef)(letters);
|
|
93
|
+
lettersRef.current = letters;
|
|
94
|
+
const onLetterChangeRef = (0, react_1.useRef)(onLetterChange);
|
|
95
|
+
onLetterChangeRef.current = onLetterChange;
|
|
96
|
+
// 进入动画:下一帧切换到 active,触发 CSS transition
|
|
97
|
+
(0, react_1.useEffect)(() => {
|
|
98
|
+
const id = requestAnimationFrame(() => {
|
|
99
|
+
// 再套一层,确保初始 transform 已被浏览器应用
|
|
100
|
+
requestAnimationFrame(() => setPhase("active"));
|
|
101
|
+
});
|
|
102
|
+
return () => cancelAnimationFrame(id);
|
|
103
|
+
}, []);
|
|
104
|
+
// 触发退出动画,真正的 onClose 在 transitionend 中触发
|
|
105
|
+
const triggerClose = () => {
|
|
106
|
+
if (phase === "exiting")
|
|
107
|
+
return;
|
|
108
|
+
// 进入动画未完成前退出:进入态和退出态 transform 相同,
|
|
109
|
+
// 切到 exiting 不会产生 transform 变化,transitionend 不会触发;
|
|
110
|
+
// 此时面板尚未滑入,直接调用 onClose 即可
|
|
111
|
+
if (phase === "entering") {
|
|
112
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setPhase("exiting");
|
|
116
|
+
};
|
|
117
|
+
// 监听 inner 的 transform 过渡结束,退出阶段调用 onClose
|
|
118
|
+
const handleTransitionEnd = (e) => {
|
|
119
|
+
if (e.target !== e.currentTarget)
|
|
120
|
+
return;
|
|
121
|
+
if (e.propertyName !== "transform")
|
|
122
|
+
return;
|
|
123
|
+
if (phase === "exiting")
|
|
124
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
125
|
+
};
|
|
126
|
+
// 选中城市:回调后触发退出动画
|
|
127
|
+
const handleSelect = (city) => {
|
|
128
|
+
var _a;
|
|
129
|
+
const province = data_1.provinceData[city.pcode];
|
|
130
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect({
|
|
131
|
+
name: city.name,
|
|
132
|
+
code: city.code,
|
|
133
|
+
province: {
|
|
134
|
+
name: (_a = province === null || province === void 0 ? void 0 : province.name) !== null && _a !== void 0 ? _a : "",
|
|
135
|
+
code: city.pcode,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
triggerClose();
|
|
139
|
+
};
|
|
140
|
+
const measureSections = () => {
|
|
141
|
+
const listEl = listRef.current;
|
|
142
|
+
if (!listEl)
|
|
143
|
+
return;
|
|
144
|
+
const offsets = [];
|
|
145
|
+
for (const letter of lettersRef.current) {
|
|
146
|
+
const el = sectionRefs.current[letter];
|
|
147
|
+
if (el)
|
|
148
|
+
offsets.push({ letter, top: el.offsetTop });
|
|
149
|
+
}
|
|
150
|
+
sectionOffsetsRef.current = offsets;
|
|
151
|
+
};
|
|
152
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
153
|
+
if (isSearching)
|
|
154
|
+
return;
|
|
155
|
+
measureSections();
|
|
156
|
+
const listEl = listRef.current;
|
|
157
|
+
if (!listEl || typeof ResizeObserver === "undefined")
|
|
158
|
+
return;
|
|
159
|
+
const ro = new ResizeObserver(() => measureSections());
|
|
160
|
+
ro.observe(listEl);
|
|
161
|
+
return () => ro.disconnect();
|
|
162
|
+
}, [isSearching]);
|
|
163
|
+
// 根据 scrollTop 通过二分查找定位当前粘滞字母
|
|
164
|
+
const getLetterByScrollTop = (scrollTop) => {
|
|
165
|
+
const offsets = sectionOffsetsRef.current;
|
|
166
|
+
if (offsets.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
let lo = 0;
|
|
169
|
+
let hi = offsets.length - 1;
|
|
170
|
+
let idx = 0;
|
|
171
|
+
// 1px 容差
|
|
172
|
+
const target = scrollTop + 1;
|
|
173
|
+
while (lo <= hi) {
|
|
174
|
+
const mid = (lo + hi) >> 1;
|
|
175
|
+
if (offsets[mid].top <= target) {
|
|
176
|
+
idx = mid;
|
|
177
|
+
lo = mid + 1;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
hi = mid - 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return offsets[idx].letter;
|
|
184
|
+
};
|
|
185
|
+
// 滚动列表到对应字母区块的上边缘
|
|
186
|
+
const scrollToLetter = (letter) => {
|
|
187
|
+
const listEl = listRef.current;
|
|
188
|
+
const sectionEl = sectionRefs.current[letter];
|
|
189
|
+
if (!listEl || !sectionEl)
|
|
190
|
+
return;
|
|
191
|
+
const target = sectionEl.offsetTop;
|
|
192
|
+
// scrollTop 未变化时不会派发 scroll 事件,
|
|
193
|
+
// 若此时置 flag 为 true 会导致后续首次真实滚动被误吞
|
|
194
|
+
if (listEl.scrollTop === target)
|
|
195
|
+
return;
|
|
196
|
+
isTouchScrollRef.current = true;
|
|
197
|
+
listEl.scrollTop = target;
|
|
198
|
+
};
|
|
199
|
+
(0, react_1.useEffect)(() => {
|
|
200
|
+
const el = sidebarRef.current;
|
|
201
|
+
if (!el)
|
|
202
|
+
return;
|
|
203
|
+
let lastLetter = null;
|
|
204
|
+
const updateByY = (clientY) => {
|
|
205
|
+
var _a;
|
|
206
|
+
const list = lettersRef.current;
|
|
207
|
+
if (list.length === 0)
|
|
208
|
+
return;
|
|
209
|
+
const rect = el.getBoundingClientRect();
|
|
210
|
+
const itemHeight = rect.height / list.length;
|
|
211
|
+
let index = Math.floor((clientY - rect.top) / itemHeight);
|
|
212
|
+
if (index < 0)
|
|
213
|
+
index = 0;
|
|
214
|
+
if (index > list.length - 1)
|
|
215
|
+
index = list.length - 1;
|
|
216
|
+
const letter = list[index];
|
|
217
|
+
if (letter !== lastLetter) {
|
|
218
|
+
lastLetter = letter;
|
|
219
|
+
setActiveLetter(letter);
|
|
220
|
+
scrollToLetter(letter);
|
|
221
|
+
(_a = onLetterChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onLetterChangeRef, letter);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// 优先使用触摸事件;不支持触摸才回退到鼠标事件,避免两套并存冲突
|
|
225
|
+
const isTouch = typeof window !== "undefined" &&
|
|
226
|
+
("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
227
|
+
if (isTouch) {
|
|
228
|
+
const handleTouchStart = (e) => {
|
|
229
|
+
setTouching(true);
|
|
230
|
+
updateByY(e.touches[0].clientY);
|
|
231
|
+
};
|
|
232
|
+
const handleTouchMove = (e) => {
|
|
233
|
+
// 阻止滚动,需要非 passive 监听器
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
updateByY(e.touches[0].clientY);
|
|
236
|
+
};
|
|
237
|
+
const handleTouchEnd = () => {
|
|
238
|
+
setTouching(false);
|
|
239
|
+
lastLetter = null;
|
|
240
|
+
};
|
|
241
|
+
el.addEventListener("touchstart", handleTouchStart, { passive: false });
|
|
242
|
+
el.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
243
|
+
el.addEventListener("touchend", handleTouchEnd);
|
|
244
|
+
el.addEventListener("touchcancel", handleTouchEnd);
|
|
245
|
+
return () => {
|
|
246
|
+
el.removeEventListener("touchstart", handleTouchStart);
|
|
247
|
+
el.removeEventListener("touchmove", handleTouchMove);
|
|
248
|
+
el.removeEventListener("touchend", handleTouchEnd);
|
|
249
|
+
el.removeEventListener("touchcancel", handleTouchEnd);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// 鼠标端:按下后拖动,move/up 绑在 document 以支持拖出 sidebar
|
|
253
|
+
let dragging = false;
|
|
254
|
+
const handleMouseDown = (e) => {
|
|
255
|
+
if (e.button !== 0)
|
|
256
|
+
return;
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
dragging = true;
|
|
259
|
+
setTouching(true);
|
|
260
|
+
updateByY(e.clientY);
|
|
261
|
+
};
|
|
262
|
+
const handleMouseMove = (e) => {
|
|
263
|
+
if (!dragging)
|
|
264
|
+
return;
|
|
265
|
+
updateByY(e.clientY);
|
|
266
|
+
};
|
|
267
|
+
const handleMouseUp = () => {
|
|
268
|
+
if (!dragging)
|
|
269
|
+
return;
|
|
270
|
+
dragging = false;
|
|
271
|
+
setTouching(false);
|
|
272
|
+
lastLetter = null;
|
|
273
|
+
};
|
|
274
|
+
el.addEventListener("mousedown", handleMouseDown);
|
|
275
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
276
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
277
|
+
return () => {
|
|
278
|
+
el.removeEventListener("mousedown", handleMouseDown);
|
|
279
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
280
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
281
|
+
};
|
|
282
|
+
}, [isSearching]);
|
|
283
|
+
// 手动滚动城市列表时,根据当前最靠上的区块激活对应字母(rAF 节流)
|
|
284
|
+
(0, react_1.useEffect)(() => {
|
|
285
|
+
if (isSearching)
|
|
286
|
+
return;
|
|
287
|
+
const listEl = listRef.current;
|
|
288
|
+
if (!listEl)
|
|
289
|
+
return;
|
|
290
|
+
let rafId = 0;
|
|
291
|
+
const handleScroll = () => {
|
|
292
|
+
// 触摸导航主动设置 scrollTop 时不覆盖 activeLetter
|
|
293
|
+
if (isTouchScrollRef.current) {
|
|
294
|
+
isTouchScrollRef.current = false;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (rafId)
|
|
298
|
+
return;
|
|
299
|
+
rafId = requestAnimationFrame(() => {
|
|
300
|
+
rafId = 0;
|
|
301
|
+
const letter = getLetterByScrollTop(listEl.scrollTop);
|
|
302
|
+
if (!letter)
|
|
303
|
+
return;
|
|
304
|
+
setActiveLetter((prev) => (prev === letter ? prev : letter));
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
listEl.addEventListener("scroll", handleScroll, { passive: true });
|
|
308
|
+
return () => {
|
|
309
|
+
listEl.removeEventListener("scroll", handleScroll);
|
|
310
|
+
if (rafId)
|
|
311
|
+
cancelAnimationFrame(rafId);
|
|
312
|
+
};
|
|
313
|
+
}, [isSearching]);
|
|
314
|
+
return ((0, jsx_runtime_1.jsx)("div", { css: [style.container], children: (0, jsx_runtime_1.jsxs)("div", { css: [
|
|
315
|
+
style.inner,
|
|
316
|
+
phase === "entering" && style.innerEnter,
|
|
317
|
+
phase === "active" && style.innerActive,
|
|
318
|
+
phase === "exiting" && style.innerExit,
|
|
319
|
+
], onTransitionEnd: handleTransitionEnd, children: [(0, jsx_runtime_1.jsxs)(Col_1.ColStart, { css: style.original, alignItems: "stretch", children: [(0, jsx_runtime_1.jsx)("div", { css: style.top, children: (0, jsx_runtime_1.jsxs)(Row_1.RowStart, { children: [(0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 1024 1024", css: style.icon, children: (0, jsx_runtime_1.jsx)("path", { d: "M896 870.4l-128-128c55.467-68.267 89.6-149.333 89.6-238.933 0-98.134-38.4-192-110.933-264.534-149.334-149.333-384-149.333-533.334-4.266-145.066 145.066-145.066 384 0 529.066 72.534 72.534 166.4 110.934 264.534 110.934 89.6 0 174.933-29.867 238.933-89.6l128 128c4.267 4.266 12.8 8.533 21.333 8.533s17.067-4.267 21.334-8.533c17.066-8.534 17.066-29.867 8.533-42.667zM260.267 721.067c-119.467-123.734-119.467-320 0-439.467 59.733-59.733 140.8-89.6 217.6-89.6 81.066 0 157.866 29.867 217.6 89.6 59.733 59.733 89.6 136.533 89.6 217.6 0 81.067-34.134 162.133-89.6 217.6-55.467 59.733-132.267 93.867-217.6 93.867-81.067 0-157.867-34.134-217.6-89.6z" }) }), (0, jsx_runtime_1.jsx)("input", { css: style.input, placeholder: "\u8BF7\u8F93\u5165\u5B57\u6BCD\u6216\u6C49\u5B57\u641C\u7D22\u57CE\u5E02", value: keyword, onChange: (e) => setKeyword(e.target.value), onCompositionStart: () => setComposing(true), onCompositionEnd: (e) => {
|
|
320
|
+
setComposing(false);
|
|
321
|
+
setKeyword(e.currentTarget.value);
|
|
322
|
+
} }), (0, jsx_runtime_1.jsx)(Clickable_1.Clickable, { css: style.exit, onClick: triggerClose, children: "\u9000\u51FA" })] }) }), !isSearching && locatedCity && ((0, jsx_runtime_1.jsxs)(Clickable_1.Clickable, { css: style.locate, onClick: () => handleSelect(locatedCity), children: [(0, jsx_runtime_1.jsxs)("svg", { viewBox: "0 0 1024 1024", css: style.locateIcon, children: [(0, jsx_runtime_1.jsx)("path", { d: "M512 128C352 128 224 256 224 416c0 115.2 70.4 204.8 160 320 32 44.8 70.4 89.6 102.4 147.2C492.8 889.6 499.2 896 512 896l0 0c12.8 0 19.2-6.4 25.6-12.8 32-51.2 70.4-96 102.4-140.8 89.6-121.6 160-211.2 160-326.4C800 256 672 128 512 128zM588.8 704c-25.6 32-51.2 64-76.8 102.4-25.6-38.4-57.6-76.8-76.8-108.8-83.2-108.8-147.2-192-147.2-281.6C288 294.4 390.4 192 512 192s224 102.4 224 224C736 505.6 672 595.2 588.8 704z" }), (0, jsx_runtime_1.jsx)("path", { d: "M512 416m-96 0a1.5 1.5 0 1 0 192 0 1.5 1.5 0 1 0-192 0Z" })] }), (0, jsx_runtime_1.jsx)("span", { css: style.locateLabel, children: "\u5F53\u524D\u5B9A\u4F4D" }), (0, jsx_runtime_1.jsx)("span", { css: style.locateName, children: locatedCity.name })] })), isSearching ? (searchResult.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { css: style.searchList, children: searchResult.map((item) => ((0, jsx_runtime_1.jsx)(Clickable_1.Clickable, { css: style.item, onClick: () => handleSelect(item), children: item.name }, item.code))) })) : ((0, jsx_runtime_1.jsx)("div", { css: style.empty, children: "\u6CA1\u6709\u5339\u914D\u7684\u57CE\u5E02" }))) : ((0, jsx_runtime_1.jsx)("div", { ref: listRef, css: style.list, children: letters.map((k) => {
|
|
323
|
+
return (
|
|
324
|
+
// 每个字母对应一个区块,包含该字母开头的城市列表
|
|
325
|
+
(0, jsx_runtime_1.jsxs)("div", { ref: (el) => {
|
|
326
|
+
sectionRefs.current[k] = el;
|
|
327
|
+
}, children: [(0, jsx_runtime_1.jsx)("div", { css: style.title, children: k.toUpperCase() }), data_1.cityData[k].map((item) => ((0, jsx_runtime_1.jsx)(Clickable_1.Clickable, { css: style.item, onClick: () => handleSelect(item), children: item.name }, item.code)))] }, k));
|
|
328
|
+
}) }))] }), !isSearching && ((0, jsx_runtime_1.jsx)("div", { ref: sidebarRef, css: style.sidebar, children: letters.map((k) => ((0, jsx_runtime_1.jsx)("div", { css: [style.letter, activeLetter === k && style.letterActive], children: k.toUpperCase() }, k))) })), touching && activeLetter && !isSearching && ((0, jsx_runtime_1.jsx)("div", { css: style.bigLetter, children: activeLetter.toUpperCase() }))] }) }));
|
|
329
|
+
}
|
|
330
|
+
function showCitySelect(options = {}) {
|
|
331
|
+
const container = (0, dom_1.createPortalDOM)();
|
|
332
|
+
container.mount((0, jsx_runtime_1.jsx)(CitySelect, Object.assign({}, options, { onClose: () => {
|
|
333
|
+
container.unmount();
|
|
334
|
+
} })));
|
|
335
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchCity = searchCity;
|
|
4
|
+
const data_1 = require("./data");
|
|
5
|
+
/**
|
|
6
|
+
* 城市搜索:根据输入的关键词前缀匹配 pinyin / pinyinShort / name 字段
|
|
7
|
+
* - 输入为空返回空数组
|
|
8
|
+
* - 英文字母不区分大小写
|
|
9
|
+
* - 使用首字符索引,避免遍历全部城市,保持 O(k) 级别的候选规模
|
|
10
|
+
*/
|
|
11
|
+
const __allCities = Object.keys(data_1.cityData)
|
|
12
|
+
.sort()
|
|
13
|
+
.flatMap((k) => data_1.cityData[k]);
|
|
14
|
+
// 按首字符建立索引:同一个城市的 pinyin、pinyinShort、name 首字符分别入桶
|
|
15
|
+
const __cityIndex = (() => {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
const put = (key, item) => {
|
|
18
|
+
if (!key)
|
|
19
|
+
return;
|
|
20
|
+
let arr = map.get(key);
|
|
21
|
+
if (!arr) {
|
|
22
|
+
arr = [];
|
|
23
|
+
map.set(key, arr);
|
|
24
|
+
}
|
|
25
|
+
// 同一 key 下去重(同一城市多字段首字符相同时只入一次)
|
|
26
|
+
if (arr[arr.length - 1] !== item)
|
|
27
|
+
arr.push(item);
|
|
28
|
+
};
|
|
29
|
+
for (const item of __allCities) {
|
|
30
|
+
put(item.pinyin[0], item);
|
|
31
|
+
put(item.pinyinShort[0], item);
|
|
32
|
+
put(item.name[0], item);
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
})();
|
|
36
|
+
function searchCity(keyword) {
|
|
37
|
+
if (!keyword)
|
|
38
|
+
return [];
|
|
39
|
+
const kw = keyword.trim();
|
|
40
|
+
if (!kw)
|
|
41
|
+
return [];
|
|
42
|
+
const first = kw[0];
|
|
43
|
+
// 英文按小写匹配 pinyin / pinyinShort;中文按原字符匹配 name
|
|
44
|
+
const isAscii = first.charCodeAt(0) < 128;
|
|
45
|
+
const lookupKey = isAscii ? first.toLowerCase() : first;
|
|
46
|
+
const candidates = __cityIndex.get(lookupKey);
|
|
47
|
+
if (!candidates)
|
|
48
|
+
return [];
|
|
49
|
+
if (isAscii) {
|
|
50
|
+
const lower = kw.toLowerCase();
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const item of candidates) {
|
|
54
|
+
if (seen.has(item))
|
|
55
|
+
continue;
|
|
56
|
+
if (item.pinyin.startsWith(lower) || item.pinyinShort.startsWith(lower)) {
|
|
57
|
+
seen.add(item);
|
|
58
|
+
out.push(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
// 中文前缀匹配 name
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const item of candidates) {
|
|
66
|
+
if (item.name.startsWith(kw))
|
|
67
|
+
out.push(item);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
// 设计变量(除 primary 外集中管理)
|
|
7
|
+
const textPrimary = "#1f2328";
|
|
8
|
+
const textSecondary = "#6b7280";
|
|
9
|
+
const textTertiary = "#9ca3af";
|
|
10
|
+
const bgPage = "#ffffff";
|
|
11
|
+
const bgSubtle = "#f5f6f8";
|
|
12
|
+
const border = "#e5e7eb";
|
|
13
|
+
const fontStack = '-apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
|
|
14
|
+
// 将 #rrggbb 颜色按比例变暗,用于派生 primaryActive
|
|
15
|
+
function darken(hex, amount) {
|
|
16
|
+
const m = hex.replace("#", "");
|
|
17
|
+
if (m.length !== 6)
|
|
18
|
+
return hex;
|
|
19
|
+
const r = parseInt(m.slice(0, 2), 16);
|
|
20
|
+
const g = parseInt(m.slice(2, 4), 16);
|
|
21
|
+
const b = parseInt(m.slice(4, 6), 16);
|
|
22
|
+
const f = (v) => Math.max(0, Math.min(255, Math.round(v * (1 - amount))));
|
|
23
|
+
const toHex = (v) => f(v).toString(16).padStart(2, "0");
|
|
24
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
25
|
+
}
|
|
26
|
+
// 根据 primary 色值生成一整套样式;primaryActive 由 primary 派生
|
|
27
|
+
function createStyle(primary) {
|
|
28
|
+
const primaryActive = darken(primary, 0.15);
|
|
29
|
+
return {
|
|
30
|
+
container: (0, react_1.css)({
|
|
31
|
+
position: "fixed",
|
|
32
|
+
top: 0,
|
|
33
|
+
left: 0,
|
|
34
|
+
width: "100%",
|
|
35
|
+
height: "100%",
|
|
36
|
+
zIndex: 9999,
|
|
37
|
+
overflow: "hidden",
|
|
38
|
+
userSelect: "none",
|
|
39
|
+
color: textPrimary,
|
|
40
|
+
fontFamily: fontStack,
|
|
41
|
+
WebkitFontSmoothing: "antialiased",
|
|
42
|
+
MozOsxFontSmoothing: "grayscale",
|
|
43
|
+
}),
|
|
44
|
+
inner: (0, react_1.css)({
|
|
45
|
+
position: "absolute",
|
|
46
|
+
top: 0,
|
|
47
|
+
left: 0,
|
|
48
|
+
width: "100%",
|
|
49
|
+
height: "100%",
|
|
50
|
+
backgroundColor: bgPage,
|
|
51
|
+
transition: "transform .3s cubic-bezier(.22,.61,.36,1)",
|
|
52
|
+
willChange: "transform",
|
|
53
|
+
}),
|
|
54
|
+
innerEnter: (0, react_1.css)({
|
|
55
|
+
transform: "translateX(100%)",
|
|
56
|
+
}),
|
|
57
|
+
innerActive: (0, react_1.css)({
|
|
58
|
+
transform: "translateX(0)",
|
|
59
|
+
}),
|
|
60
|
+
innerExit: (0, react_1.css)({
|
|
61
|
+
transform: "translateX(100%)",
|
|
62
|
+
}),
|
|
63
|
+
sidebar: (0, react_1.css)({
|
|
64
|
+
position: "absolute",
|
|
65
|
+
top: "50%",
|
|
66
|
+
right: ".12rem",
|
|
67
|
+
transform: "translateY(-50%)",
|
|
68
|
+
zIndex: 2,
|
|
69
|
+
padding: ".1rem .06rem",
|
|
70
|
+
borderRadius: ".3rem",
|
|
71
|
+
display: "flex",
|
|
72
|
+
flexDirection: "column",
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
gap: ".02rem",
|
|
75
|
+
}),
|
|
76
|
+
letter: (0, react_1.css)({
|
|
77
|
+
width: ".36rem",
|
|
78
|
+
height: ".36rem",
|
|
79
|
+
lineHeight: ".36rem",
|
|
80
|
+
textAlign: "center",
|
|
81
|
+
fontSize: ".22rem",
|
|
82
|
+
fontFamily: "Arial, sans-serif",
|
|
83
|
+
fontWeight: 500,
|
|
84
|
+
borderRadius: "50%",
|
|
85
|
+
color: textSecondary,
|
|
86
|
+
transition: "background-color .15s, color .15s, transform .15s",
|
|
87
|
+
}),
|
|
88
|
+
letterActive: (0, react_1.css)({
|
|
89
|
+
backgroundColor: primary,
|
|
90
|
+
color: "#fff",
|
|
91
|
+
transform: "scale(1.08)",
|
|
92
|
+
}),
|
|
93
|
+
bigLetter: (0, react_1.css)({
|
|
94
|
+
zIndex: 3,
|
|
95
|
+
position: "absolute",
|
|
96
|
+
fontFamily: "Arial, sans-serif",
|
|
97
|
+
top: "50%",
|
|
98
|
+
left: "50%",
|
|
99
|
+
transform: "translate(-50%, -50%)",
|
|
100
|
+
backgroundColor: "rgba(17,24,39,0.6)",
|
|
101
|
+
color: "#fff",
|
|
102
|
+
fontSize: ".6rem",
|
|
103
|
+
width: "1.4rem",
|
|
104
|
+
height: "1.4rem",
|
|
105
|
+
lineHeight: "1.4rem",
|
|
106
|
+
textAlign: "center",
|
|
107
|
+
borderRadius: ".24rem",
|
|
108
|
+
pointerEvents: "none",
|
|
109
|
+
}),
|
|
110
|
+
original: (0, react_1.css)({
|
|
111
|
+
height: "100%",
|
|
112
|
+
backgroundColor: bgPage,
|
|
113
|
+
}),
|
|
114
|
+
top: (0, react_1.css)({
|
|
115
|
+
padding: ".24rem .3rem",
|
|
116
|
+
borderBottom: `1px solid ${border}`,
|
|
117
|
+
"& > div": {
|
|
118
|
+
height: ".72rem",
|
|
119
|
+
backgroundColor: bgSubtle,
|
|
120
|
+
padding: "0 .24rem",
|
|
121
|
+
borderRadius: ".36rem",
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
icon: (0, react_1.css)({
|
|
125
|
+
width: ".32rem",
|
|
126
|
+
height: ".32rem",
|
|
127
|
+
fill: textTertiary,
|
|
128
|
+
marginRight: ".15rem",
|
|
129
|
+
fontSize: 0,
|
|
130
|
+
flexShrink: 0,
|
|
131
|
+
}),
|
|
132
|
+
input: (0, react_1.css)({
|
|
133
|
+
flexGrow: 1,
|
|
134
|
+
height: "100%",
|
|
135
|
+
minWidth: 0,
|
|
136
|
+
fontSize: ".28rem",
|
|
137
|
+
lineHeight: ".72rem",
|
|
138
|
+
color: textPrimary,
|
|
139
|
+
fontFamily: "inherit",
|
|
140
|
+
border: "none",
|
|
141
|
+
outline: "none",
|
|
142
|
+
backgroundColor: "transparent",
|
|
143
|
+
padding: 0,
|
|
144
|
+
"&::placeholder": {
|
|
145
|
+
color: textTertiary,
|
|
146
|
+
lineHeight: ".72rem",
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
exit: (0, react_1.css)({
|
|
150
|
+
whiteSpace: "nowrap",
|
|
151
|
+
marginLeft: ".2rem",
|
|
152
|
+
lineHeight: ".72rem",
|
|
153
|
+
fontSize: ".28rem",
|
|
154
|
+
color: primary,
|
|
155
|
+
cursor: "pointer",
|
|
156
|
+
transition: "opacity .15s, color .15s",
|
|
157
|
+
"&:active": {
|
|
158
|
+
color: primaryActive,
|
|
159
|
+
opacity: 0.75,
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
list: (0, react_1.css)({
|
|
163
|
+
height: 0,
|
|
164
|
+
flexGrow: 1,
|
|
165
|
+
overflow: "auto",
|
|
166
|
+
position: "relative",
|
|
167
|
+
WebkitOverflowScrolling: "touch",
|
|
168
|
+
}),
|
|
169
|
+
locate: (0, react_1.css)({
|
|
170
|
+
display: "flex",
|
|
171
|
+
alignItems: "center",
|
|
172
|
+
padding: ".2rem .23rem",
|
|
173
|
+
borderBottom: `1px solid ${border}`,
|
|
174
|
+
backgroundColor: bgPage,
|
|
175
|
+
transition: "background-color .12s",
|
|
176
|
+
"&:active": {
|
|
177
|
+
backgroundColor: bgSubtle,
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
locateIcon: (0, react_1.css)({
|
|
181
|
+
width: ".32rem",
|
|
182
|
+
height: ".32rem",
|
|
183
|
+
fill: primary,
|
|
184
|
+
marginRight: ".16rem",
|
|
185
|
+
fontSize: 0,
|
|
186
|
+
flexShrink: 0,
|
|
187
|
+
}),
|
|
188
|
+
locateLabel: (0, react_1.css)({
|
|
189
|
+
fontSize: ".24rem",
|
|
190
|
+
color: textTertiary,
|
|
191
|
+
marginRight: ".16rem",
|
|
192
|
+
}),
|
|
193
|
+
locateName: (0, react_1.css)({
|
|
194
|
+
fontSize: ".24rem",
|
|
195
|
+
color: primary,
|
|
196
|
+
}),
|
|
197
|
+
title: (0, react_1.css)({
|
|
198
|
+
padding: ".08rem .3rem",
|
|
199
|
+
fontSize: ".22rem",
|
|
200
|
+
fontWeight: 600,
|
|
201
|
+
color: textSecondary,
|
|
202
|
+
letterSpacing: ".02rem",
|
|
203
|
+
textTransform: "uppercase",
|
|
204
|
+
backgroundColor: bgSubtle,
|
|
205
|
+
position: "sticky",
|
|
206
|
+
top: 0,
|
|
207
|
+
zIndex: 1,
|
|
208
|
+
}),
|
|
209
|
+
item: (0, react_1.css)({
|
|
210
|
+
padding: ".24rem .3rem",
|
|
211
|
+
fontSize: ".3rem",
|
|
212
|
+
color: textPrimary,
|
|
213
|
+
borderBottom: `1px solid ${border}`,
|
|
214
|
+
backgroundColor: bgPage,
|
|
215
|
+
transition: "background-color .12s",
|
|
216
|
+
"&:active": {
|
|
217
|
+
backgroundColor: bgSubtle,
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
searchList: (0, react_1.css)({
|
|
221
|
+
height: 0,
|
|
222
|
+
flexGrow: 1,
|
|
223
|
+
overflow: "auto",
|
|
224
|
+
WebkitOverflowScrolling: "touch",
|
|
225
|
+
}),
|
|
226
|
+
empty: (0, react_1.css)({
|
|
227
|
+
height: 0,
|
|
228
|
+
flexGrow: 1,
|
|
229
|
+
display: "flex",
|
|
230
|
+
alignItems: "center",
|
|
231
|
+
justifyContent: "center",
|
|
232
|
+
color: textTertiary,
|
|
233
|
+
fontSize: ".26rem",
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
exports.DEFAULT_PRIMARY = "#2f7dff";
|