227 lines
6.8 KiB
TypeScript
227 lines
6.8 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
REGION_ALL,
|
|
getCities,
|
|
getCitiesForPicker,
|
|
getDistricts,
|
|
getDistrictsForPicker,
|
|
getProvincesForPicker,
|
|
normalizeRegionSelection,
|
|
toCityLevelRegion,
|
|
type RegionSelection,
|
|
} from '../lib/region-data';
|
|
|
|
type RegionPickerProps = {
|
|
open: boolean;
|
|
value: RegionSelection;
|
|
onClose: () => void;
|
|
onConfirm: (region: RegionSelection) => void;
|
|
/** 2 = 仅省/市(门店列表);3 = 省/市/区(地址等) */
|
|
levels?: 2 | 3;
|
|
};
|
|
|
|
type PickerLevel = 'province' | 'city' | 'district';
|
|
|
|
const ALL_TABS: Array<{ key: PickerLevel; label: string }> = [
|
|
{ key: 'province', label: '省份' },
|
|
{ key: 'city', label: '城市' },
|
|
{ key: 'district', label: '区县' },
|
|
];
|
|
|
|
function initialTab(value: RegionSelection, levels: 2 | 3): PickerLevel {
|
|
const normalized = levels === 2 ? toCityLevelRegion(value) : normalizeRegionSelection(value);
|
|
if (levels === 2) {
|
|
return normalized.province && normalized.province !== REGION_ALL ? 'city' : 'province';
|
|
}
|
|
if (normalized.district && normalized.district !== REGION_ALL) return 'district';
|
|
if (normalized.city && normalized.city !== REGION_ALL) return 'city';
|
|
return 'province';
|
|
}
|
|
|
|
function tabLabel(tab: PickerLevel, draft: RegionSelection, fallback: string) {
|
|
if (tab === 'province') {
|
|
return draft.province && draft.province !== REGION_ALL ? draft.province : fallback;
|
|
}
|
|
if (tab === 'city') {
|
|
return draft.city && draft.city !== REGION_ALL ? draft.city : fallback;
|
|
}
|
|
return draft.district && draft.district !== REGION_ALL ? draft.district : fallback;
|
|
}
|
|
|
|
export default function RegionPicker({
|
|
open,
|
|
value,
|
|
onClose,
|
|
onConfirm,
|
|
levels = 3,
|
|
}: RegionPickerProps) {
|
|
const [draft, setDraft] = useState<RegionSelection>(value);
|
|
const [activeTab, setActiveTab] = useState<PickerLevel>('province');
|
|
const listRef = useRef<HTMLDivElement>(null);
|
|
|
|
const tabs = levels === 2 ? ALL_TABS.slice(0, 2) : ALL_TABS;
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const normalized = levels === 2 ? toCityLevelRegion(value) : normalizeRegionSelection(value);
|
|
setDraft(normalized);
|
|
setActiveTab(initialTab(value, levels));
|
|
}, [open, value, levels]);
|
|
|
|
const listItems = useMemo(() => {
|
|
if (activeTab === 'province') return getProvincesForPicker();
|
|
if (activeTab === 'city') return getCitiesForPicker(draft.province);
|
|
return getDistrictsForPicker(draft.province, draft.city);
|
|
}, [activeTab, draft.province, draft.city]);
|
|
|
|
const selectedValue =
|
|
activeTab === 'province' ? draft.province : activeTab === 'city' ? draft.city : draft.district;
|
|
|
|
const canConfirm =
|
|
levels === 2
|
|
? Boolean(draft.province && draft.city)
|
|
: Boolean(draft.province && draft.city && draft.district);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
scrollActiveIntoView(listRef.current, selectedValue);
|
|
}, [open, activeTab, selectedValue, listItems.length]);
|
|
|
|
if (!open) return null;
|
|
|
|
function scrollActiveIntoView(container: HTMLDivElement | null, label: string) {
|
|
if (!container || !label) return;
|
|
const active = container.querySelector<HTMLElement>(`[data-label="${CSS.escape(label)}"]`);
|
|
active?.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
function selectProvince(province: string) {
|
|
if (province === REGION_ALL) {
|
|
setDraft({ province: REGION_ALL, city: REGION_ALL, district: REGION_ALL });
|
|
setActiveTab('city');
|
|
return;
|
|
}
|
|
const nextCities = getCities(province);
|
|
const city = nextCities[0] ?? '';
|
|
if (levels === 2) {
|
|
setDraft({
|
|
province,
|
|
city,
|
|
district: REGION_ALL,
|
|
});
|
|
setActiveTab('city');
|
|
return;
|
|
}
|
|
const nextDistricts = getDistricts(province, city);
|
|
setDraft({
|
|
province,
|
|
city,
|
|
district: nextDistricts[0] ?? '',
|
|
});
|
|
setActiveTab('city');
|
|
}
|
|
|
|
function selectCity(city: string) {
|
|
if (city === REGION_ALL) {
|
|
setDraft({ ...draft, city: REGION_ALL, district: REGION_ALL });
|
|
if (levels === 3) setActiveTab('district');
|
|
return;
|
|
}
|
|
if (levels === 2) {
|
|
setDraft({
|
|
...draft,
|
|
city,
|
|
district: REGION_ALL,
|
|
});
|
|
return;
|
|
}
|
|
const nextDistricts = getDistricts(draft.province, city);
|
|
setDraft({
|
|
...draft,
|
|
city,
|
|
district: nextDistricts[0] ?? '',
|
|
});
|
|
setActiveTab('district');
|
|
}
|
|
|
|
function selectDistrict(district: string) {
|
|
setDraft({ ...draft, district });
|
|
}
|
|
|
|
function onSelectItem(item: string) {
|
|
if (activeTab === 'province') selectProvince(item);
|
|
else if (activeTab === 'city') selectCity(item);
|
|
else selectDistrict(item);
|
|
}
|
|
|
|
function onTabClick(tab: PickerLevel) {
|
|
if (tab === 'city' && !draft.province) return;
|
|
if (tab === 'district' && (!draft.province || !draft.city)) return;
|
|
setActiveTab(tab);
|
|
}
|
|
|
|
function handleConfirm() {
|
|
if (!canConfirm) return;
|
|
const next = levels === 2 ? toCityLevelRegion(draft) : normalizeRegionSelection(draft);
|
|
onConfirm(next);
|
|
}
|
|
|
|
return (
|
|
<div className="region-picker-overlay" role="presentation" onClick={onClose}>
|
|
<div
|
|
className="region-picker-sheet"
|
|
role="dialog"
|
|
aria-label="选择地区"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="region-picker-toolbar">
|
|
<div className="region-picker-tabs" role="tablist">
|
|
{tabs.map((tab) => {
|
|
const disabled =
|
|
(tab.key === 'city' && !draft.province) ||
|
|
(tab.key === 'district' && (!draft.province || !draft.city));
|
|
return (
|
|
<button
|
|
key={tab.key}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={activeTab === tab.key}
|
|
disabled={disabled}
|
|
className={`region-picker-tab${activeTab === tab.key ? ' active' : ''}`}
|
|
onClick={() => onTabClick(tab.key)}
|
|
>
|
|
{tabLabel(tab.key, draft, tab.label)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={`region-picker-confirm${canConfirm ? ' ready' : ''}`}
|
|
disabled={!canConfirm}
|
|
onClick={handleConfirm}
|
|
>
|
|
确定
|
|
</button>
|
|
</div>
|
|
|
|
<div className="region-picker-list" ref={listRef}>
|
|
{listItems.map((item) => (
|
|
<button
|
|
key={item}
|
|
type="button"
|
|
data-label={item}
|
|
className={`region-picker-option${selectedValue === item ? ' selected' : ''}${
|
|
item === REGION_ALL ? ' region-picker-option--all' : ''
|
|
}`}
|
|
onClick={() => onSelectItem(item)}
|
|
>
|
|
{item}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|