242 lines
7.7 KiB
TypeScript
242 lines
7.7 KiB
TypeScript
import { BaseEventOrig, InputProps, ScrollView } from '@tarojs/components';
|
|
import Taro from '@tarojs/taro';
|
|
|
|
import { Search } from '@taroify/core';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
import { CITY_CODE_TO_NAME_MAP, CITY_INDEXES_LIST, GROUP_CITY_INDEXES_LIST } from '@/constants/city';
|
|
import { logWithPrefix } from '@/utils/common';
|
|
|
|
import './index.less';
|
|
|
|
interface Item {
|
|
cityCode: number | string;
|
|
cityName: string;
|
|
keyword: string;
|
|
}
|
|
|
|
const PREFIX = 'search-city';
|
|
const HOT_CITY = [
|
|
{ cityCode: 110100, cityName: '北京' },
|
|
{ cityCode: 310100, cityName: '上海' },
|
|
{ cityCode: 440100, cityName: '广州' },
|
|
{ cityCode: 440300, cityName: '深圳' },
|
|
{ cityCode: 330100, cityName: '杭州' },
|
|
{ cityCode: 430100, cityName: '长沙' },
|
|
{ cityCode: 420100, cityName: '武汉' },
|
|
{ cityCode: 350200, cityName: '厦门' },
|
|
{ cityCode: 610100, cityName: '西安' },
|
|
{ cityCode: 410100, cityName: '郑州' },
|
|
{ cityCode: 510100, cityName: '成都' },
|
|
{ cityCode: 340100, cityName: '合肥' },
|
|
];
|
|
|
|
const OFFSET_INDEX_SIZE = 2;
|
|
const log = logWithPrefix(PREFIX);
|
|
const realtimeLogger = Taro.getRealtimeLogManager();
|
|
realtimeLogger.tag(PREFIX);
|
|
const useHeight = () => {
|
|
const [winHeight, setWinHeight] = useState(0);
|
|
const [indexItemHeight, setIndexItemHeight] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const windowInfo = Taro.getWindowInfo();
|
|
const windowHeight = windowInfo.windowHeight;
|
|
setWinHeight(windowHeight);
|
|
// 上下预留两个选项高度的空白
|
|
setIndexItemHeight(Math.floor(windowHeight / (26 + OFFSET_INDEX_SIZE * 2)));
|
|
}, []);
|
|
|
|
return [winHeight, indexItemHeight];
|
|
};
|
|
export interface SearchCityProps {
|
|
onSelectCity: (cityCode: string) => void;
|
|
currentCity?: string;
|
|
forGroup?: boolean;
|
|
banner?: string;
|
|
offset?: number;
|
|
}
|
|
|
|
export default function SearchCity({
|
|
onSelectCity,
|
|
currentCity = '',
|
|
banner = '',
|
|
forGroup = false,
|
|
offset = 0,
|
|
}: SearchCityProps) {
|
|
const [winHeight, indexItemHeight] = useHeight();
|
|
const [touchAnchor, setTouchAnchor] = useState<string | undefined>();
|
|
const [touchMoving, setTouchMoving] = useState(false);
|
|
const [searchResult, setSearchResult] = useState<Item[]>([]);
|
|
const showSearchList = searchResult.length > 0;
|
|
const CITY_LIST = forGroup ? GROUP_CITY_INDEXES_LIST : CITY_INDEXES_LIST;
|
|
|
|
const handleSearchChange = useCallback((event: BaseEventOrig<InputProps.inputEventDetail>) => {
|
|
const value = event.detail.value;
|
|
log('handleSearchChange', value);
|
|
if (!value) {
|
|
setSearchResult([]);
|
|
return;
|
|
}
|
|
const result: Item[] = [];
|
|
CITY_LIST.forEach(obj => {
|
|
obj.data.forEach(city => {
|
|
if (city.keyword.includes(value.toLocaleUpperCase())) {
|
|
result.push({ ...city });
|
|
}
|
|
});
|
|
});
|
|
setSearchResult(result);
|
|
}, []);
|
|
|
|
const handleSelectCity = useCallback(
|
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
const cityCode = e.currentTarget.dataset.code;
|
|
onSelectCity(String(cityCode));
|
|
},
|
|
[onSelectCity]
|
|
);
|
|
|
|
const handleTouchStart = useCallback(
|
|
(e: React.TouchEvent<HTMLDivElement>) => {
|
|
const pageY = e.touches[0].pageY;
|
|
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
|
|
if (index < 0 || index >= CITY_LIST.length) {
|
|
return;
|
|
}
|
|
const item = CITY_LIST[index];
|
|
if (item) {
|
|
setTouchMoving(true);
|
|
setTouchAnchor(item.letter);
|
|
}
|
|
},
|
|
[indexItemHeight]
|
|
);
|
|
|
|
const handleTouchMove = useCallback(
|
|
(e: React.TouchEvent<HTMLDivElement>) => {
|
|
const pageY = e.touches[0].pageY;
|
|
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
|
|
if (index < 0 || index >= CITY_LIST.length) {
|
|
return;
|
|
}
|
|
const item = CITY_LIST[index];
|
|
item && setTouchAnchor(item.letter);
|
|
},
|
|
[indexItemHeight]
|
|
);
|
|
|
|
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLDivElement>) => {
|
|
e.stopPropagation();
|
|
setTouchMoving(false);
|
|
log('touch end');
|
|
}, []);
|
|
|
|
const handleClickAnchor = useCallback((anchor: string) => {
|
|
setTouchAnchor(anchor);
|
|
log('click anchor', anchor);
|
|
}, []);
|
|
|
|
return (
|
|
<div className={PREFIX}>
|
|
<ScrollView scrollY style={{ height: winHeight - offset }} scrollIntoView={touchAnchor}>
|
|
<div className={`${PREFIX}__search-wrapper ${forGroup ? 'group' : ''}`}>
|
|
{forGroup && banner ? (
|
|
<div className={`${PREFIX}__banner`}>
|
|
<div className="dash"></div>
|
|
<div className="text">{banner}</div>
|
|
<div className="dash"></div>
|
|
</div>
|
|
) : null}
|
|
<Search
|
|
className={`${PREFIX}__search`}
|
|
placeholder="输入城市名称"
|
|
shape="rounded"
|
|
onChange={handleSearchChange}
|
|
/>
|
|
</div>
|
|
{showSearchList && (
|
|
<div className={`${PREFIX}__search-list`}>
|
|
{searchResult.map(city => (
|
|
<div
|
|
key={city.cityCode}
|
|
className={`${PREFIX}__indexes-cell`}
|
|
data-code={city.cityCode}
|
|
onClick={handleSelectCity}
|
|
>
|
|
{city.cityName}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{!showSearchList && (
|
|
<div>
|
|
<div className={`${PREFIX}__position-title`}>当前城市</div>
|
|
<div className={`${PREFIX}__position-city`}>{CITY_CODE_TO_NAME_MAP.get(currentCity)}</div>
|
|
<div className={`${PREFIX}__hot-city-title`}>热门城市</div>
|
|
<div className={`${PREFIX}__hot-city-container`}>
|
|
{HOT_CITY.map(city => (
|
|
<div
|
|
key={city.cityCode}
|
|
className={`${PREFIX}__hot-city-item`}
|
|
data-code={city.cityCode}
|
|
onClick={handleSelectCity}
|
|
>
|
|
{city.cityName}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className={`${PREFIX}__indexes-list`}>
|
|
{CITY_LIST.map(item => {
|
|
return (
|
|
<div key={item.letter} className={`${PREFIX}__indexes-fragment`}>
|
|
<div className={`${PREFIX}__indexes-anchor`} id={item.letter}>
|
|
{item.letter}
|
|
</div>
|
|
{item.data.map(city => (
|
|
<div
|
|
key={city.cityCode}
|
|
className={`${PREFIX}__indexes-cell`}
|
|
data-code={city.cityCode}
|
|
onClick={handleSelectCity}
|
|
>
|
|
{city.cityName}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</ScrollView>
|
|
<div>
|
|
{!showSearchList && (
|
|
<div
|
|
className={`${PREFIX}__indexes-bar`}
|
|
onTouchStart={handleTouchStart}
|
|
onTouchMove={handleTouchMove}
|
|
onTouchEnd={handleTouchEnd}
|
|
onTouchCancel={handleTouchEnd}
|
|
style={{ top: indexItemHeight * OFFSET_INDEX_SIZE + (forGroup ? 72 : 0) }}
|
|
>
|
|
{CITY_LIST.map(item => {
|
|
return (
|
|
<div
|
|
key={item.letter}
|
|
className={`${PREFIX}__indexes-bar-item`}
|
|
style={{ height: indexItemHeight }}
|
|
onClick={() => handleClickAnchor(item.letter)}
|
|
>
|
|
{item.letter}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{touchAnchor && touchMoving && <div className={`${PREFIX}__indexes-index-alert`}>{touchAnchor}</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|