// 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 });