import { createPortal } from 'react-dom'; import { useEffect, useMemo, useRef, useState } from 'react'; import { codeToText } from 'element-china-area-data'; import { CHINA_REGION_OPTIONS, DEFAULT_REGION_CODES, formatRegionLabel, parseRegionCodes, } from '../lib/china-region'; type RegionNode = { value: string; label: string; children?: RegionNode[]; }; type ChinaRegionPickerProps = { value?: string[]; onChange?: (codes: string[]) => void; disabled?: boolean; }; const STEP_LABELS = ['选择省份', '选择城市', '选择区县'] as const; function lockBodyScroll(lock: boolean) { if (typeof document === 'undefined') return; const body = document.body; if (lock) { const scrollY = window.scrollY; body.dataset.dukangScrollY = String(scrollY); body.style.position = 'fixed'; body.style.top = `-${scrollY}px`; body.style.left = '0'; body.style.right = '0'; body.style.overflow = 'hidden'; } else { const scrollY = Number(body.dataset.dukangScrollY ?? '0'); body.style.position = ''; body.style.top = ''; body.style.left = ''; body.style.right = ''; body.style.overflow = ''; delete body.dataset.dukangScrollY; window.scrollTo(0, scrollY); } } function buildInitialDraft(value: string[]): { draft: string[]; step: number } { if (value.length === 3) { return { draft: [...value], step: 2 }; } if (value.length > 0) { const draft = [...value]; for (let i = value.length; i < DEFAULT_REGION_CODES.length; i += 1) { draft.push(DEFAULT_REGION_CODES[i]); } return { draft, step: Math.min(value.length, 2) }; } return { draft: [...DEFAULT_REGION_CODES], step: 1 }; } export default function ChinaRegionPicker({ value = [], onChange, disabled }: ChinaRegionPickerProps) { const [open, setOpen] = useState(false); const [draft, setDraft] = useState(value); const [step, setStep] = useState(0); const listRef = useRef(null); const activeItemRef = useRef(null); const options = CHINA_REGION_OPTIONS as RegionNode[]; const parsed = parseRegionCodes(value); const display = parsed ? formatRegionLabel(parsed) : '请选择省 / 市 / 区县'; const provinces = options; const cities = useMemo(() => { const p = provinces.find((item) => item.value === draft[0]); return p?.children ?? []; }, [draft, provinces]); const districts = useMemo(() => { const c = cities.find((item) => item.value === draft[1]); return c?.children ?? []; }, [draft, cities]); const listItems = step === 0 ? provinces : step === 1 ? cities : districts; const activeCode = draft[step]; useEffect(() => { if (!open) return; lockBodyScroll(true); return () => lockBodyScroll(false); }, [open]); useEffect(() => { if (!open) return; const timer = window.setTimeout(() => { const container = listRef.current; const activeEl = activeItemRef.current; if (!container || !activeEl) return; const offset = activeEl.offsetTop - container.clientHeight / 2 + activeEl.clientHeight / 2; container.scrollTo({ top: Math.max(0, offset), behavior: 'smooth' }); }, 0); return () => window.clearTimeout(timer); }, [open, step, activeCode, listItems]); function openPicker() { if (disabled) return; const { draft: next, step: initialStep } = buildInitialDraft(value); setDraft(next); setStep(initialStep); setOpen(true); } function closePicker() { setOpen(false); } function selectItem(code: string) { const next = [...draft.slice(0, step), code]; setDraft(next); if (step < 2) { setStep(step + 1); return; } onChange?.(next); closePicker(); } function confirm() { if (draft.length < 3) return; onChange?.(draft); closePicker(); } const modal = open ? (
e.stopPropagation()} onTouchMove={(e) => e.stopPropagation()} >
选择地区
{STEP_LABELS.map((label, index) => ( ))}
{draft[0] && (

{codeToText[draft[0]]} {draft[1] ? ` / ${codeToText[draft[1]]}` : ''} {draft[2] ? ` / ${codeToText[draft[2]]}` : ''}

)}
{listItems.map((item) => ( ))}
) : null; return ( <> {typeof document !== 'undefined' ? createPortal(modal, document.body) : null} ); }