const { useState: useStateRecommend, useEffect: useEffectRecommend, useMemo: useMemoRecommend, useRef: useRefRecommend } = React;
const RECOMMEND_FLOOR_PLAN_OPTIONS = ['1R', '1K', '1DK', '1LDK', '2K', '2DK', '2LDK', '3K', '3DK', '3LDK', '4K', '4DK', '4LDK'];
const RECOMMEND_DEFAULT_VISIBLE_COUNT = 5;
const RECOMMEND_RENT_MIN = 40000;
const RECOMMEND_RENT_MAX = 500000;
const RECOMMEND_RENT_STEP = 5000;
const RECOMMEND_CONTACT_METHODS = [
{ key: 'wechat', label: '微信号' },
{ key: 'phone', label: '手机号码' },
];
const RECOMMEND_LEAFLET_JS = 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js';
const RECOMMEND_LEAFLET_CSS = 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css';
let recommendLeafletPromise = null;
function createRecommendFilters(regionKey = '') {
return {
contactMethod: 'wechat',
contactValue: '',
regionKey: regionKey || '',
areaKey: '',
districtName: '',
operatorName: '',
lineId: '',
station: '',
keyword: '',
minRent: RECOMMEND_RENT_MIN,
maxRent: RECOMMEND_RENT_MAX,
floorPlans: [],
vacancyOnly: false,
};
}
function normalizeRecommendText(value) {
return String(value || '').trim().toLowerCase();
}
function loadRecommendLeaflet() {
if (window.L) return Promise.resolve(window.L);
if (recommendLeafletPromise) return recommendLeafletPromise;
recommendLeafletPromise = new Promise((resolve, reject) => {
if (!document.querySelector(`link[href="${RECOMMEND_LEAFLET_CSS}"]`)) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = RECOMMEND_LEAFLET_CSS;
document.head.appendChild(link);
}
const existing = document.querySelector(`script[src="${RECOMMEND_LEAFLET_JS}"]`);
if (existing) {
existing.addEventListener('load', () => resolve(window.L));
existing.addEventListener('error', reject);
return;
}
const script = document.createElement('script');
script.src = RECOMMEND_LEAFLET_JS;
script.async = true;
script.onload = () => resolve(window.L);
script.onerror = reject;
document.head.appendChild(script);
});
return recommendLeafletPromise;
}
function extractRecommendFloorPlanTokens(value) {
return (String(value || '').toUpperCase().match(/\d+(?:R|K|DK|LDK)/g) || []);
}
function recommendFloorPlanScore(value) {
return RECOMMEND_FLOOR_PLAN_OPTIONS.indexOf(String(value || '').toUpperCase());
}
function recommendPropertyMatchesFloorPlans(floorPlanText, selectedPlans) {
if (!selectedPlans?.length) return true;
const normalized = String(floorPlanText || '').toUpperCase().replace(/\s+/g, '');
if (!normalized) return false;
if (selectedPlans.some((plan) => normalized.includes(String(plan).toUpperCase()))) {
return true;
}
const rangeText = normalized.split('/')[0] || normalized;
const tokens = extractRecommendFloorPlanTokens(rangeText);
if (tokens.length >= 2 && rangeText.includes('~')) {
const start = recommendFloorPlanScore(tokens[0]);
const end = recommendFloorPlanScore(tokens[tokens.length - 1]);
if (start >= 0 && end >= 0) {
const min = Math.min(start, end);
const max = Math.max(start, end);
return selectedPlans.some((plan) => {
const score = recommendFloorPlanScore(plan);
return score >= min && score <= max;
});
}
}
return false;
}
function recommendPropertyMatchesStation(property, stationName) {
const normalizedStation = normalizeRecommendText(stationName);
if (!normalizedStation) return true;
return (property.stations || []).some((item) => normalizeRecommendText(item) === normalizedStation)
|| normalizeRecommendText(property.accessText).includes(normalizedStation)
|| normalizeRecommendText(property.detailTransportText).includes(normalizedStation);
}
function recommendPropertyMatchesLine(property, line) {
if (!line) return true;
const normalizedLineName = normalizeRecommendText(line.lineName);
if (!normalizedLineName) return true;
if (
normalizeRecommendText(property.accessText).includes(normalizedLineName)
|| normalizeRecommendText(property.detailTransportText).includes(normalizedLineName)
) {
return true;
}
const propertyStationSet = new Set((property.stations || []).map((item) => normalizeRecommendText(item)).filter(Boolean));
return (line.stations || []).some((station) => propertyStationSet.has(normalizeRecommendText(station.stationName)));
}
function recommendPropertyMatchesOperator(property, operator) {
if (!operator?.lines?.length) return true;
return operator.lines.some((line) => recommendPropertyMatchesLine(property, line));
}
function parseRecommendRentRange(value) {
return parseRentRange(String(value || '').replace(/[((][^))]*[))]/g, '').trim());
}
function formatRecommendManYen(value) {
const amount = Number(value || 0);
if (!Number.isFinite(amount) || amount <= 0) return '0万';
const man = amount / 10000;
if (man < 10) return `${man.toFixed(1)}万`;
return Number.isInteger(man) ? `${man}万` : `${man.toFixed(1)}万`;
}
function filterRecommendProperties(properties, filters, detailedOperators, stationDirectory) {
if (!filters?.regionKey) return [];
const currentOperator = detailedOperators.find((item) => item.operatorName === filters.operatorName) || null;
const currentLine = (stationDirectory?.lines || []).find((item) => item.lineId === filters.lineId) || null;
const keyword = normalizeRecommendText(filters.keyword);
return properties
.filter((item) => {
if (filters.areaKey && item.areaKey !== filters.areaKey) return false;
if (filters.districtName && normalizeDistrictName(item.districtName || item.skcs) !== filters.districtName) return false;
if (filters.operatorName && currentOperator && !recommendPropertyMatchesOperator(item, currentOperator)) return false;
if (filters.lineId && currentLine && !recommendPropertyMatchesLine(item, currentLine)) return false;
if (filters.station && !recommendPropertyMatchesStation(item, filters.station)) return false;
const rentRange = parseRecommendRentRange(item.detailRentText || item.rent || item.rent);
if (rentRange.max < Number(filters.minRent)) return false;
if (rentRange.min > Number(filters.maxRent)) return false;
if (filters.vacancyOnly && !item.hasVacancy) return false;
if (!recommendPropertyMatchesFloorPlans(item.floorPlanText, filters.floorPlans)) return false;
if (keyword) {
const haystack = [
item.name,
item.districtName,
item.skcs,
item.areaName,
item.address,
item.propertyKey,
item.accessText,
item.detailTransportText,
...(item.stations || []),
];
if (!haystack.some((value) => normalizeRecommendText(value).includes(keyword))) {
return false;
}
}
return true;
})
.sort((left, right) => parseRecommendRentRange(left.detailRentText || left.rent).min - parseRecommendRentRange(right.detailRentText || right.rent).min);
}
function RecommendScreen({ initialRegionKey = '', onOpenListingRoom, onOpenWatchProperty, stateSnapshot, onStateChange }) {
const initialState = stateSnapshot || {
prompt: '',
answer: '',
filters: null,
properties: [],
resultCount: 0,
visibleCount: RECOMMEND_DEFAULT_VISIBLE_COUNT,
resultView: 'list',
mapLocations: [],
scrollTop: 0,
anchorPropertyKey: '',
anchorRestorePending: false,
};
const [prompt, setPrompt] = useStateRecommend(() => initialState.prompt || '');
const [answer, setAnswer] = useStateRecommend(() => initialState.answer || '');
const [filters, setFilters] = useStateRecommend(() => initialState.filters || null);
const [properties, setProperties] = useStateRecommend(() => initialState.properties || []);
const [resultCount, setResultCount] = useStateRecommend(() => Number(initialState.resultCount || 0) || 0);
const [visibleCount, setVisibleCount] = useStateRecommend(() => Number(initialState.visibleCount) || RECOMMEND_DEFAULT_VISIBLE_COUNT);
const [resultView, setResultView] = useStateRecommend(() => initialState.resultView === 'map' ? 'map' : 'list');
const [mapLocations, setMapLocations] = useStateRecommend(() => Array.isArray(initialState.mapLocations) ? initialState.mapLocations : []);
const [mapLoading, setMapLoading] = useStateRecommend(false);
const [mapError, setMapError] = useStateRecommend('');
const [loading, setLoading] = useStateRecommend(false);
const [error, setError] = useStateRecommend('');
const scrollRef = useRefRecommend(null);
const restoredScrollRef = useRefRecommend(false);
const latestStateRef = useRefRecommend(initialState);
const visibleProperties = properties.slice(0, visibleCount);
const canLoadMore = properties.length > visibleCount;
useEffectRecommend(() => {
latestStateRef.current = {
prompt,
answer,
filters,
properties,
resultCount,
visibleCount,
resultView,
mapLocations,
scrollTop: scrollRef.current?.scrollTop || 0,
anchorPropertyKey: latestStateRef.current?.anchorPropertyKey || '',
anchorRestorePending: latestStateRef.current?.anchorRestorePending || false,
};
onStateChange && onStateChange(latestStateRef.current);
}, [answer, filters, mapLocations, onStateChange, prompt, properties, resultCount, resultView, visibleCount]);
useEffectRecommend(() => {
if (restoredScrollRef.current || stateSnapshot?.anchorRestorePending) return;
restoredScrollRef.current = true;
const nextScrollTop = Number(stateSnapshot?.scrollTop || 0);
if (!nextScrollTop) return;
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
if (scrollRef.current) scrollRef.current.scrollTop = nextScrollTop;
});
});
}, []);
useEffectRecommend(() => (
() => {
if (!onStateChange) return;
const current = latestStateRef.current || {};
onStateChange({
...current,
scrollTop: scrollRef.current?.scrollTop || 0,
});
}
), [onStateChange]);
useEffectRecommend(() => {
if (!stateSnapshot?.anchorRestorePending) return;
const targetKey = stateSnapshot.anchorPropertyKey;
if (!targetKey || !scrollRef.current) return;
const targetElement = scrollRef.current.querySelector(`[data-recommend-card-key="${targetKey}"]`);
if (!targetElement) return;
const container = scrollRef.current;
const rawTop = targetElement.offsetTop - Math.max(0, (container.clientHeight - targetElement.offsetHeight) / 2);
const maxTop = Math.max(0, container.scrollHeight - container.clientHeight);
const nextTop = Math.max(0, Math.min(rawTop, maxTop));
window.requestAnimationFrame(() => {
container.scrollTop = nextTop;
const nextSnapshot = {
...latestStateRef.current,
scrollTop: nextTop,
anchorRestorePending: false,
};
latestStateRef.current = nextSnapshot;
onStateChange && onStateChange(nextSnapshot);
});
}, [onStateChange, stateSnapshot?.anchorPropertyKey, stateSnapshot?.anchorRestorePending, visibleProperties.length]);
function rememberRecommendState(anchorPropertyKey = '') {
const snapshot = {
prompt,
answer,
filters,
properties,
resultCount,
visibleCount,
resultView,
mapLocations,
scrollTop: scrollRef.current?.scrollTop || 0,
anchorPropertyKey,
anchorRestorePending: Boolean(anchorPropertyKey),
};
latestStateRef.current = snapshot;
onStateChange && onStateChange(snapshot);
}
async function handleSubmit() {
const nextPrompt = String(prompt || '').trim();
if (!nextPrompt) {
setError('请先输入你的找房需求。');
return;
}
if (nextPrompt.length > 200) {
setError('需求最多输入 200 个字。');
return;
}
setError('');
setAnswer('');
setFilters(null);
setProperties([]);
setResultCount(0);
setVisibleCount(RECOMMEND_DEFAULT_VISIBLE_COUNT);
setResultView('list');
setMapLocations([]);
setMapError('');
setLoading(true);
try {
const result = await subscribeApiFetch('/ai-recommend', {
method: 'POST',
body: { prompt: nextPrompt },
});
setAnswer(result.answer || '');
setFilters(result.filters || null);
setProperties(Array.isArray(result.properties) ? result.properties : []);
setResultCount(Number(result.resultCount || 0) || 0);
setVisibleCount(RECOMMEND_DEFAULT_VISIBLE_COUNT);
setResultView('list');
setMapLocations([]);
setMapError('');
} catch (issue) {
setAnswer('');
setFilters(null);
setProperties([]);
setResultCount(0);
setResultView('list');
setMapLocations([]);
setMapError('');
setError(issue.message === 'rate_limited' ? '请求有点频繁,请稍后再试。' : 'AI 荐房暂时没有成功,请稍后再试。');
} finally {
setLoading(false);
}
}
function resetDraft() {
setPrompt('');
setAnswer('');
setFilters(null);
setProperties([]);
setResultCount(0);
setVisibleCount(RECOMMEND_DEFAULT_VISIBLE_COUNT);
setResultView('list');
setMapLocations([]);
setMapError('');
setError('');
}
async function openMapView() {
setResultView('map');
if (!properties.length || mapLocations.length > 0 || mapLoading) return;
setMapError('');
setMapLoading(true);
try {
const result = await subscribeApiFetch('/map-locations', {
method: 'POST',
body: {
properties: properties.slice(0, 20).map((property) => ({
propertyKey: property.propertyKey,
name: property.name,
regionName: property.regionName,
districtName: property.districtName || property.skcs || property.areaName,
skcs: property.skcs,
areaName: property.areaName,
address: property.address,
rent: property.rent,
detailRentText: property.detailRentText,
hasVacancy: property.hasVacancy,
activeRoomCount: property.activeRoomCount,
roomCount: property.roomCount,
rooms: property.rooms || [],
})),
},
});
setMapLocations(Array.isArray(result.locations) ? result.locations : []);
} catch (issue) {
setMapError(issue.message === 'rate_limited' ? '请求有点频繁,请稍后再试。' : '地图位置暂时加载失败,请稍后再试。');
} finally {
setMapLoading(false);
}
}
function useExample(text) {
setPrompt(text);
setError('');
}
const promptLength = String(prompt || '').length;
return (
{
latestStateRef.current = {
prompt,
answer,
filters,
properties,
resultCount,
visibleCount,
resultView,
mapLocations,
scrollTop: scrollRef.current?.scrollTop || 0,
};
}}
>
{[
'福冈东区目前可租的2室以上',
'东京20万以内靠近地铁的1LDK',
'大阪现在有空房的便宜2DK',
].map((item) => (
))}
{error ? {error}
: null}
{answer || properties.length > 0 || filters ? (
{properties.length > 0 ? (
) : null}
{answer ? {answer}
: null}
{loading ? (
正在理解需求并筛选公开房源…
) : properties.length === 0 ? (
当前没有匹配物件,可以换个地区、预算或放宽房型再试。
) : resultView === 'map' ? (
{
const selectedProperty = properties.find((item) => item.propertyKey === selectedPropertyKey);
rememberRecommendState(selectedPropertyKey);
onOpenWatchProperty && onOpenWatchProperty(selectedProperty?.propertyKey || selectedPropertyKey);
}}
/>
) : (
<>
{visibleProperties.map((property) => (
{
rememberRecommendState(selectedProperty.propertyKey);
onOpenWatchProperty && onOpenWatchProperty(selectedProperty.propertyKey);
}}
onOpenRoom={(selectedProperty, room) => {
rememberRecommendState(selectedProperty.propertyKey);
onOpenListingRoom && onOpenListingRoom(selectedProperty, room);
}}
/>
))}
{canLoadMore ? (
) : null}
>
)}
) : null}
);
}
function RecommendRentSlider({ min, max, onChange }) {
const minPercent = ((min - RECOMMEND_RENT_MIN) / (RECOMMEND_RENT_MAX - RECOMMEND_RENT_MIN)) * 100;
const maxPercent = ((max - RECOMMEND_RENT_MIN) / (RECOMMEND_RENT_MAX - RECOMMEND_RENT_MIN)) * 100;
function handleMinChange(value) {
const nextMin = Math.min(Number(value), max - RECOMMEND_RENT_STEP);
onChange({ min: nextMin, max });
}
function handleMaxChange(value) {
const nextMax = Math.max(Number(value), min + RECOMMEND_RENT_STEP);
onChange({ min, max: nextMax });
}
return (
最低
¥{formatRecommendManYen(min)}
最高
¥{formatRecommendManYen(max)}
4.0万
20万
35万
50万
);
}
function RecommendPropertyCard({ property, onOpenProperty, onOpenRoom }) {
const activeRooms = (property.rooms || []).filter((room) => room.status !== 'expired');
if (activeRooms.length > 0) {
return (
onOpenRoom && onOpenRoom(selectedProperty, room)}
/>
);
}
const thumbUrl = resolveImg(property.propertyImageLocalUrl || property.propertyImageRemoteUrl || property.image);
const jpyCnyRate = window.UrExchange.useJpyCnyRate();
const renderYenWithCny = (value) => window.UrExchange.renderYenWithCny(value, jpyCnyRate, {
fontSize: '0.76em',
marginLeft: 4,
opacity: 0.82,
});
return (
{thumbUrl ? (

) : (
)}
{property.regionName || '—'} · {property.districtName || property.skcs || property.areaName || '—'}
{property.address || property.accessText || property.detailTransportText || '—'}
{renderYenWithCny(property.detailRentText || property.rent || '—')}
房型 {property.floorPlanText || '—'}
{(property.stations || []).slice(0, 3).map((station) => (
{station}站
))}
{property.areaName ? {property.areaName} : null}
);
}
function escapeRecommendHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function RecommendMapView({ locations, loading, error, onOpenProperty }) {
const mapRef = useRefRecommend(null);
const mapInstanceRef = useRefRecommend(null);
useEffectRecommend(() => {
if (!mapRef.current || loading || error || !locations.length) return undefined;
let disposed = false;
loadRecommendLeaflet().then((L) => {
if (disposed || !mapRef.current) return;
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
const first = locations[0];
const map = L.map(mapRef.current, {
zoomControl: true,
attributionControl: true,
}).setView([first.lat, first.lng], 12);
mapInstanceRef.current = map;
L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '地理院タイル',
}).addTo(map);
const bounds = [];
locations.forEach((location) => {
const lat = Number(location.lat);
const lng = Number(location.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
bounds.push([lat, lng]);
const popup = `
${escapeRecommendHtml(location.name)}
${escapeRecommendHtml(location.districtName || location.address || '')}
${escapeRecommendHtml(location.rent || '')}
`;
L.marker([lat, lng]).addTo(map).bindPopup(popup);
});
if (bounds.length > 1) {
map.fitBounds(bounds, { padding: [28, 28], maxZoom: 14 });
}
window.requestAnimationFrame(() => map.invalidateSize());
}).catch(() => {
if (mapRef.current) {
mapRef.current.innerHTML = '地图组件加载失败,请稍后再试。
';
}
});
return () => {
disposed = true;
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, [error, loading, locations]);
useEffectRecommend(() => {
const current = mapRef.current;
if (!current) return undefined;
const handleClick = (event) => {
const target = event.target?.closest?.('[data-map-property-key]');
if (!target) return;
const propertyKey = target.getAttribute('data-map-property-key');
if (propertyKey) onOpenProperty && onOpenProperty(propertyKey);
};
current.addEventListener('click', handleClick);
return () => current.removeEventListener('click', handleClick);
}, [onOpenProperty]);
if (loading) {
return 正在解析物件地址并加载地图…
;
}
if (error) {
return {error}
;
}
if (!locations.length) {
return 这些物件暂时没有可用坐标,先用列表查看更稳。
;
}
return (
地图使用国土地理院免费底图,位置由地址解析生成,可能不是楼栋精确点。
);
}
function RecommendPropertyDetail({ property, onBack, onOpenRoom }) {
const [galleryIndex, setGalleryIndex] = useStateRecommend(0);
const mergedProperty = property || {};
const activeRooms = (mergedProperty.rooms || []).filter((room) => room.status !== 'expired');
const jpyCnyRate = window.UrExchange.useJpyCnyRate();
const renderYenWithCny = (value) => window.UrExchange.renderYenWithCny(value, jpyCnyRate, {
fontSize: '0.76em',
marginLeft: 4,
opacity: 0.8,
});
const gallery = useMemoRecommend(() => {
const images = [];
const seen = new Set();
const pushImage = (url, label) => {
const resolved = resolveImg(url);
if (!resolved || seen.has(resolved)) return;
seen.add(resolved);
images.push({ url: resolved, label });
};
(mergedProperty.galleryImages || []).forEach((item, index) => {
pushImage(item.localUrl || item.url || item.remoteUrl, normalizeWatchGalleryLabel(item, index));
});
pushImage(mergedProperty.propertyImageLocalUrl || mergedProperty.propertyImageRemoteUrl || mergedProperty.image, '物件外观');
return images;
}, [mergedProperty]);
return (
物业详情
{mergedProperty.name || mergedProperty.propertyKey}
{gallery[galleryIndex] ? (
![{gallery[galleryIndex].label}]({gallery[galleryIndex].url})
) : (
)}
{gallery.length > 1 ? (
{gallery.map((item, index) => (
))}
) : null}
{mergedProperty.regionName || '—'} · {mergedProperty.districtName || mergedProperty.skcs || mergedProperty.areaName || '—'}
{mergedProperty.name || '—'}
{mergedProperty.address || '—'}
交通信息
{mergedProperty.detailTransportText || mergedProperty.accessText || '—'}
{activeRooms.length > 0 ? (
当前可租房间
{activeRooms.map((room) => (
))}
) : (
当前状态
当前没有可租房间,可以先了解物业信息,后续再回来查看。
)}
);
}
function DetailStat({ label, value }) {
return (
);
}
const recommendStyles = {
shell: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', background: '#FAF8F5', overflow: 'hidden' },
scroll: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '14px 14px calc(28px + env(safe-area-inset-bottom, 0px))' },
formCard: { background: '#FFFFFF', borderRadius: 20, border: '1px solid #EDE6D9', padding: '18px 16px', boxShadow: '0 2px 10px rgba(60, 50, 30, 0.04)' },
resultsCard: { marginTop: 14, background: '#FFFFFF', borderRadius: 20, border: '1px solid #EDE6D9', padding: '18px 16px', boxShadow: '0 2px 10px rgba(60, 50, 30, 0.04)' },
sectionHead: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 14 },
sectionTitle: { fontSize: 17, fontWeight: 800, color: '#1A1A1A' },
mapToggleButton: { flexShrink: 0, minHeight: 34, padding: '0 12px', borderRadius: 999, border: '1px solid #E4DFD6', background: '#FAF6EE', color: '#1A1A1A', fontSize: 12, fontWeight: 800, fontFamily: 'inherit', cursor: 'pointer' },
formGrid: { display: 'grid', gap: 12 },
aiField: { display: 'grid', gap: 8 },
aiTextarea: { width: '100%', minHeight: 128, resize: 'vertical', borderRadius: 16, border: '1px solid #E4DFD6', background: '#FFFCF8', padding: '14px 14px', fontSize: 15, lineHeight: 1.7, color: '#1A1A1A', fontFamily: 'inherit' },
aiMetaRow: { display: 'flex', justifyContent: 'space-between', gap: 10, color: '#8A7F72', fontSize: 11, fontWeight: 700 },
exampleRow: { display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 12 },
exampleChip: { minHeight: 34, padding: '0 12px', borderRadius: 999, border: '1px solid #E4DFD6', background: '#FAF6EE', color: '#6B6258', fontSize: 12, fontWeight: 700, fontFamily: 'inherit', cursor: 'pointer' },
aiAnswer: { marginBottom: 14, padding: '13px 14px', borderRadius: 16, background: '#FAF6EE', color: '#4F463B', fontSize: 13, lineHeight: 1.8, fontWeight: 600 },
field: { display: 'grid', gap: 7 },
fieldLabel: { fontSize: 11, color: '#8A7F72', fontWeight: 800, letterSpacing: '0.06em', textTransform: 'uppercase' },
contactMethodRow: { display: 'flex', gap: 8, flexWrap: 'wrap' },
contactMethodChip: { minHeight: 38, padding: '0 14px', borderRadius: 999, border: '1px solid #E4DFD6', background: '#FFFFFF', color: '#6B6258', fontSize: 13, fontWeight: 700, fontFamily: 'inherit', cursor: 'pointer' },
contactMethodChipActive: { background: '#1A1A1A', borderColor: '#1A1A1A', color: '#FFFFFF' },
select: { width: '100%', minHeight: 46, borderRadius: 14, border: '1px solid #E4DFD6', background: '#FFFCF8', padding: '0 14px', fontSize: 14, color: '#1A1A1A', fontFamily: 'inherit' },
input: { width: '100%', minHeight: 46, borderRadius: 14, border: '1px solid #E4DFD6', background: '#FFFCF8', padding: '0 14px', fontSize: 14, color: '#1A1A1A', fontFamily: 'inherit' },
sliderWrap: { marginTop: 14 },
sliderCard: { marginTop: 10, borderRadius: 16, border: '1px solid #EDE6D9', background: '#FAF6EE', padding: '16px 14px 14px' },
sliderValueRow: { display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'space-between' },
sliderValueBox: { flex: 1 },
sliderValueLabel: { fontSize: 11, color: '#8A7F72', fontWeight: 700 },
sliderValueMain: { marginTop: 6, fontSize: 20, fontWeight: 800, color: '#1A1A1A' },
sliderTrackWrap: { position: 'relative', marginTop: 16, padding: '10px 0' },
sliderTrackBase: { position: 'absolute', top: 20, left: 0, right: 0, height: 6, borderRadius: 999, background: '#E7DDCF' },
sliderTrackFill: { position: 'absolute', top: 20, height: 6, borderRadius: 999, background: '#C6572B' },
rangeInput: { position: 'relative', zIndex: 2, width: '100%', margin: 0, appearance: 'none', background: 'transparent', pointerEvents: 'auto' },
sliderTickRow: { display: 'flex', justifyContent: 'space-between', marginTop: 10, color: '#8A7F72', fontSize: 11, fontWeight: 600 },
toggleRow: { marginTop: 14 },
toggleCard: { display: 'flex', gap: 12, alignItems: 'center', padding: '14px', borderRadius: 16, border: '1px solid #EDE6D9', background: '#FAF6EE' },
toggleTitle: { fontSize: 14, fontWeight: 700, color: '#1A1A1A' },
floorPlanWrap: { marginTop: 14 },
chipGrid: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 10 },
planChip: { minWidth: 54, minHeight: 38, padding: '0 12px', borderRadius: 999, border: '1px solid #E4DFD6', background: '#FFFFFF', color: '#6B6258', fontSize: 13, fontWeight: 700, fontFamily: 'inherit', cursor: 'pointer' },
planChipActive: { background: '#1A1A1A', borderColor: '#1A1A1A', color: '#FFFFFF' },
error: { marginTop: 14, padding: '12px 14px', borderRadius: 14, background: '#FFF2EE', color: '#A85D37', fontSize: 12, lineHeight: 1.7 },
actionRow: { display: 'grid', gap: 10, marginTop: 16 },
secondaryButton: { width: '100%', minWidth: 0, minHeight: 46, borderRadius: 14, border: '1px solid #E4DFD6', background: '#FFFFFF', color: '#1A1A1A', fontSize: 14, fontWeight: 700, fontFamily: 'inherit', cursor: 'pointer' },
primaryButton: { width: '100%', minWidth: 0, minHeight: 50, borderRadius: 14, border: 'none', background: '#1A1A1A', color: '#FFFFFF', fontSize: 14, fontWeight: 800, fontFamily: 'inherit', cursor: 'pointer' },
pendingNotice: { marginBottom: 12, padding: '12px 14px', borderRadius: 14, background: '#FFF8EE', color: '#9A6A2C', fontSize: 12, lineHeight: 1.7 },
emptyState: { padding: '18px 6px 10px', color: '#8A7F72', fontSize: 13, lineHeight: 1.8 },
resultList: { display: 'flex', flexDirection: 'column', gap: 12 },
mapWrap: { display: 'grid', gap: 10 },
mapCanvas: { width: '100%', height: 420, minHeight: '52vh', borderRadius: 18, overflow: 'hidden', border: '1px solid #EDE6D9', background: '#F4EFE4' },
mapHint: { color: '#8A7F72', fontSize: 11, lineHeight: 1.6, fontWeight: 600 },
propertyCard: { display: 'flex', gap: 12, padding: '12px', borderRadius: 18, border: '1px solid #EDE6D9', background: '#FFFCF8' },
propertyThumb: { width: 84, height: 84, borderRadius: 14, overflow: 'hidden', background: '#F4EFE4', border: '1px solid #EDE6D9', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' },
propertyThumbImg: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },
propertyBody: { flex: 1, minWidth: 0 },
propertyTopRow: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 },
propertyName: { fontSize: 15, fontWeight: 800, color: '#1A1A1A', lineHeight: 1.35 },
vacancyBadge: { flexShrink: 0, padding: '5px 8px', borderRadius: 999, fontSize: 10, fontWeight: 800 },
propertyMeta: { marginTop: 4, fontSize: 11, color: '#8A7F72', lineHeight: 1.5 },
propertyAddress: { marginTop: 6, fontSize: 12, color: '#6B6258', lineHeight: 1.6, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' },
propertyRent: { marginTop: 8, fontSize: 13, color: '#C6572B', fontWeight: 800 },
propertySubMeta: { marginTop: 5, fontSize: 11, color: '#8A7F72' },
tagRow: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 },
tag: { display: 'inline-flex', alignItems: 'center', padding: '5px 8px', borderRadius: 999, background: '#F4EFE4', color: '#6B6258', fontSize: 10, fontWeight: 700 },
propertyActions: { display: 'flex', gap: 8, marginTop: 12 },
inlineButton: { flex: 1, minHeight: 38, borderRadius: 12, border: 'none', background: '#1A1A1A', color: '#FFFFFF', fontSize: 12, fontWeight: 800, fontFamily: 'inherit', cursor: 'pointer' },
loadMoreButton: { width: '100%', minHeight: 46, marginTop: 14, borderRadius: 14, border: '1px solid #E4DFD6', background: '#FFFFFF', color: '#1A1A1A', fontSize: 14, fontWeight: 800, fontFamily: 'inherit', cursor: 'pointer' },
detailShell: { flex: 1, display: 'flex', flexDirection: 'column', background: '#FAF8F5' },
detailTopBar: { display: 'flex', alignItems: 'center', gap: 10, padding: 'calc(12px + env(safe-area-inset-top, 0px)) 18px 12px', background: '#FAF8F5', borderBottom: '1px solid #EDE6D9' },
detailBackBtn: { width: 34, height: 34, borderRadius: 17, border: '1px solid #EDE6D9', background: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' },
detailTopTitle: { minWidth: 0 },
detailTopMain: { fontSize: 16, fontWeight: 800, color: '#1A1A1A' },
detailTopSub: { marginTop: 2, fontSize: 12, color: '#8A7F72', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
detailScroll: { flex: 1, overflowY: 'auto', padding: '14px 14px calc(110px + env(safe-area-inset-bottom, 0px))' },
detailGalleryWrap: { background: '#FFFFFF', borderRadius: 18, border: '1px solid #EDE6D9', padding: '12px', marginBottom: 14 },
detailGalleryMain: { width: '100%', aspectRatio: '1 / 0.82', borderRadius: 16, overflow: 'hidden', background: '#F4EFE4', display: 'flex', alignItems: 'center', justifyContent: 'center' },
detailGalleryImg: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },
detailGalleryFallback: { width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' },
detailThumbRow: { display: 'flex', gap: 8, overflowX: 'auto', paddingTop: 10 },
detailThumb: { width: 62, height: 62, borderRadius: 12, border: '1px solid #E4DFD6', overflow: 'hidden', background: '#FFFFFF', flexShrink: 0, padding: 0, cursor: 'pointer' },
detailThumbImg: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },
detailBlock: { background: '#FFFFFF', borderRadius: 18, border: '1px solid #EDE6D9', padding: '18px 16px', marginBottom: 14 },
detailMeta: { fontSize: 12, color: '#8A7F72', lineHeight: 1.5 },
detailName: { marginTop: 8, fontSize: 24, fontWeight: 800, color: '#1A1A1A', lineHeight: 1.2 },
detailAddress: { marginTop: 10, fontSize: 13, color: '#6B6258', lineHeight: 1.7 },
detailStatsGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 14 },
detailStatCell: { background: '#FFFFFF', borderRadius: 16, border: '1px solid #EDE6D9', padding: '14px 14px 12px' },
detailStatLabel: { fontSize: 11, color: '#8A7F72', fontWeight: 700 },
detailStatValue: { marginTop: 8, fontSize: 14, color: '#1A1A1A', fontWeight: 800, lineHeight: 1.5 },
detailSection: { background: '#FFFFFF', borderRadius: 18, border: '1px solid #EDE6D9', padding: '16px', marginBottom: 14 },
detailSectionTitle: { fontSize: 15, fontWeight: 800, color: '#1A1A1A', marginBottom: 10 },
detailBodyText: { fontSize: 13, color: '#6B6258', lineHeight: 1.8 },
detailRoomList: { display: 'flex', flexDirection: 'column', gap: 10 },
detailRoomRow: { width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, padding: '12px 0', border: 'none', borderTop: '1px dashed #F4EFE4', background: 'transparent', textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit' },
detailRoomName: { fontSize: 14, fontWeight: 700, color: '#1A1A1A' },
detailRoomMeta: { marginTop: 4, fontSize: 11, color: '#8A7F72', lineHeight: 1.6 },
};
Object.assign(window, { RecommendScreen });