// Location filter sheet — region → prefectures → areas const { useState: useStateLoc, useEffect: useEffectLoc } = React; function LocationSheet({ open, onClose, selectedRegions, selectedAreas, onChange, areasByRegion, ensureAreas, countMode = 'rooms', }) { const [activeRegion, setActiveRegion] = useStateLoc(REGIONS[1].name); const [focusedRegionKey, setFocusedRegionKey] = useStateLoc(REGIONS[1].prefs[0].key); useEffectLoc(() => { if (!open) return; const nextFocused = selectedRegions[0] || focusedRegionKey || REGIONS[1].prefs[0].key; const parentRegion = REGIONS.find(r => r.prefs.some(p => p.key === nextFocused)); if (parentRegion) setActiveRegion(parentRegion.name); setFocusedRegionKey(nextFocused); }, [open]); useEffectLoc(() => { if (!open) return; const currentGroup = REGIONS.find(r => r.name === activeRegion); if (!currentGroup) return; if (currentGroup.prefs.some(p => p.key === focusedRegionKey)) return; const preferred = selectedRegions.find(key => currentGroup.prefs.some(p => p.key === key)); setFocusedRegionKey(preferred || currentGroup.prefs[0].key); }, [activeRegion, focusedRegionKey, open, selectedRegions]); useEffectLoc(() => { if (!open || !focusedRegionKey) return; ensureAreas && ensureAreas(focusedRegionKey); }, [open, focusedRegionKey, ensureAreas]); if (!open) return null; const region = REGIONS.find(r => r.name === activeRegion); const focusedPref = region.prefs.find(p => p.key === focusedRegionKey) || region.prefs[0]; const areaState = areasByRegion[focusedPref.key] || { data: [], loading: false, error: null }; const labelOf = (key) => { for (const r of REGIONS) { const m = r.prefs.find(p => p.key === key); if (m) return m.label; } return key; }; const areaLabelOf = (areaKey) => { for (const state of Object.values(areasByRegion || {})) { const match = (state.data || []).find(item => item.areaKey === areaKey); if (match) return match.name; } return areaKey; }; const removeAreasForRegion = (areas, regionKey) => areas.filter(areaKey => !areaKey.startsWith(regionKey + '__')); const toggleRegion = (key) => { const selected = selectedRegions.includes(key); const nextRegions = selected ? selectedRegions.filter(x => x !== key) : [...selectedRegions, key]; const nextAreas = selected ? removeAreasForRegion(selectedAreas, key) : selectedAreas; onChange({ regions: nextRegions, areas: nextAreas }); }; const toggleArea = (area) => { const areaKey = area.areaKey; const regionKey = area.regionKey; const nextRegions = selectedRegions.includes(regionKey) ? selectedRegions : [...selectedRegions, regionKey]; const nextAreas = selectedAreas.includes(areaKey) ? selectedAreas.filter(x => x !== areaKey) : [...selectedAreas, areaKey]; onChange({ regions: nextRegions, areas: nextAreas }); }; const clearAll = () => { onChange({ regions: [], areas: [] }); }; const sheetContent = (
e.stopPropagation()}>
选择地区
{(selectedRegions.length > 0 || selectedAreas.length > 0) && (
{selectedRegions.map(k => (
toggleRegion(k)}> {labelOf(k)}
))} {selectedAreas.map(areaKey => (
toggleArea({ areaKey, regionKey: areaKey.split('__')[0] })}> {areaLabelOf(areaKey)}
))}
)}
{REGIONS.map(r => { const count = r.prefs.filter(p => selectedRegions.includes(p.key)).length; const active = r.name === activeRegion; return (
setActiveRegion(r.name)} > {r.name} {count > 0 && {count}}
); })}
都道府县
{region.prefs.map(p => { const on = selectedRegions.includes(p.key); const focused = p.key === focusedPref.key; const areaCount = selectedAreas.filter(areaKey => areaKey.startsWith(p.key + '__')).length; return ( ); })}
进一步细分
{focusedPref.label} 下的区域
{!selectedRegions.includes(focusedPref.key) && (
先选择 {focusedPref.label}
)}
{areaState.loading ? (
正在加载区域…
) : areaState.error ? (
区域加载失败
) : areaState.data.length === 0 ? (
当前没有更细的区域数据
) : (
{areaState.data.map(area => { const on = selectedAreas.includes(area.areaKey); const enabled = selectedRegions.includes(area.regionKey); const propertyNameCount = area.propertyCount ?? area.availablePropertyCount ?? area.totalPropertyCount ?? 0; const roomCount = area.availableRoomCount ?? area.roomCount ?? 0; const badgeText = countMode === 'properties' ? `${propertyNameCount}个物件名` : `${roomCount}套空房`; const metaText = countMode === 'properties' ? `共 ${propertyNameCount} 个物件名` : `共 ${roomCount} 套空房`; return ( ); })}
)}
); return ReactDOM.createPortal(sheetContent, document.body); } const sheetStyles = { backdrop: { position: 'fixed', inset: 0, background: 'rgba(20,18,14,0.38)', display: 'flex', alignItems: 'flex-end', zIndex: 1000, animation: 'fadeIn 180ms ease' }, sheet: { width: '100%', background: '#FAF8F5', borderTopLeftRadius: 20, borderTopRightRadius: 20, display: 'flex', flexDirection: 'column', height: '82dvh', minHeight: 520, maxHeight: '82dvh', boxShadow: '0 -8px 32px rgba(0,0,0,0.12)', animation: 'slideUp 260ms cubic-bezier(.2,.8,.2,1)' }, grabber: { width: 36, height: 4, borderRadius: 2, background: '#D4CCBE', margin: '8px auto 0' }, header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 20px 10px' }, headerTitle: { fontSize: 17, fontWeight: 600, color: '#1A1A1A', letterSpacing: '0.01em' }, closeBtn: { width: 32, height: 32, borderRadius: 16, border: 'none', background: '#EFEAE0', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, chipRow: { display: 'flex', flexWrap: 'wrap', gap: 6, padding: '6px 20px 12px', alignItems: 'center' }, chip: { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 10px', borderRadius: 999, background: '#F4EADB', fontSize: 12, color: '#8a7256', cursor: 'pointer', fontWeight: 500 }, areaChip: { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 10px', borderRadius: 999, background: '#FFF1E8', fontSize: 12, color: '#9a4e34', cursor: 'pointer', fontWeight: 600 }, clearBtn: { border: 'none', background: 'transparent', color: '#6B6258', fontSize: 12, textDecoration: 'underline', cursor: 'pointer', padding: 4 }, body: { display: 'flex', flex: 1, minHeight: 0, borderTop: '1px solid #EDE6D9' }, regionRail: { width: 120, background: '#F2EBDE', overflowY: 'auto', paddingTop: 4 }, regionItem: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 14px 14px 18px', fontSize: 14, cursor: 'pointer', borderLeft: '3px solid transparent' }, countBadge: { background: '#C6572B', color: '#fff', fontSize: 10, fontWeight: 600, minWidth: 18, height: 18, borderRadius: 9, padding: '0 5px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }, prefPanel: { flex: 1, overflowY: 'auto', padding: '16px 18px 20px' }, panelTitle: { fontSize: 12, color: '#8A7F72', fontWeight: 600, marginBottom: 10, letterSpacing: '0.06em' }, panelHint: { fontSize: 11, color: '#8A7F72', marginTop: -6, marginBottom: 2 }, prefGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }, prefBtn: { padding: '14px 10px', borderRadius: 12, border: '1px solid #E4DFD6', fontSize: 14, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', transition: 'all 150ms ease', display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'flex-start' }, prefMeta: { fontSize: 10, opacity: 0.78, fontWeight: 600 }, subsection: { marginTop: 18, paddingTop: 16, borderTop: '1px dashed #E4DFD6' }, subsectionHead: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 10 }, disabledHint: { fontSize: 11, color: '#B0A695', paddingTop: 2, whiteSpace: 'nowrap' }, stateCard: { padding: '16px 14px', background: '#FFFFFF', border: '1px solid #E4DFD6', borderRadius: 12, fontSize: 12, color: '#8A7F72' }, areaList: { display: 'flex', flexDirection: 'column', gap: 10 }, areaCard: { padding: '12px 12px 11px', borderRadius: 14, border: '1px solid #E4DFD6', background: '#FFFFFF', textAlign: 'left', fontFamily: 'inherit', cursor: 'pointer' }, areaTop: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, marginBottom: 6 }, areaName: { fontSize: 14, fontWeight: 700, color: '#1A1A1A' }, areaBadge: { flexShrink: 0, padding: '3px 8px', borderRadius: 999, fontSize: 10, fontWeight: 700 }, areaRange: { fontSize: 11, color: '#6B6258', lineHeight: 1.5 }, areaMeta: { marginTop: 6, fontSize: 11, color: '#8A7F72', fontWeight: 500 }, footer: { padding: '12px 20px 20px', borderTop: '1px solid #EDE6D9', background: '#FAF8F5' }, applyBtn: { width: '100%', padding: '15px', borderRadius: 14, border: 'none', background: '#1A1A1A', color: '#FAF8F5', fontSize: 15, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', letterSpacing: '0.02em' }, }; Object.assign(window, { LocationSheet });