const { useState: useStateWatch, useEffect: useEffectWatch, useMemo: useMemoWatch } = React; const WATCH_CONTACT_QR_URL = '/assets/wechat-qr.jpg'; const WATCH_CONTACT_WECHAT_ID = 'urrent'; function normalizeWatchGalleryLabel(item, index) { const raw = item?.label || item?.title || item?.name || item?.alt || item?.comment || ''; const text = String(raw).trim(); if (!text) return index === 0 ? '物件外观' : `物件图片 ${index + 1}`; if (text.includes('外観')) return '物件外观'; if (text.includes('周辺')) return '周边环境'; return text.length > 18 ? text.slice(0, 18) + '…' : text; } function WatchScreen({ onOpenRoom }) { const [session, setSession] = useStateWatch(getWatchSession()); const [sessionLoading, setSessionLoading] = useStateWatch(Boolean(getWatchSession()?.token)); const [authOpen, setAuthOpen] = useStateWatch(false); const [authStep, setAuthStep] = useStateWatch('email'); const [email, setEmail] = useStateWatch(''); const [code, setCode] = useStateWatch(''); const [debugCode, setDebugCode] = useStateWatch(''); const [authError, setAuthError] = useStateWatch(''); const [authLoading, setAuthLoading] = useStateWatch(false); const [contactOpen, setContactOpen] = useStateWatch(false); const [saveLabel, setSaveLabel] = useStateWatch('保存二维码'); const [subscriptions, setSubscriptions] = useStateWatch([]); const [subscriptionsLoading, setSubscriptionsLoading] = useStateWatch(false); const [watchDetailKey, setWatchDetailKey] = useStateWatch(null); const [locSheetOpen, setLocSheetOpen] = useStateWatch(false); const [rentSheetOpen, setRentSheetOpen] = useStateWatch(false); const [districtSheetOpen, setDistrictSheetOpen] = useStateWatch(false); const [locationFilter, setLocationFilter] = useStateWatch({ regions: ['tokyo'], areas: [] }); const [selectedDistricts, setSelectedDistricts] = useStateWatch([]); const [rent, setRent] = useStateWatch({ min: 0, max: MAX_RENT }); const [areasByRegion, setAreasByRegion] = useStateWatch({}); const [catalogLoading, setCatalogLoading] = useStateWatch(false); const [catalogError, setCatalogError] = useStateWatch(''); const [catalogProperties, setCatalogProperties] = useStateWatch([]); const selectedKeys = locationFilter.regions; const selectedAreaKeys = locationFilter.areas; const watchAccessEnabled = Boolean(session?.user?.watchAccessEnabled); const updateSessionUser = (nextUser) => { setSession((current) => { if (!current) return current; const nextSession = { ...current, user: { ...current.user, ...nextUser } }; setWatchSession(nextSession); return nextSession; }); }; const ensureAreas = async (regionKey) => { if (!regionKey) return; const current = areasByRegion[regionKey]; if (current?.loading || current?.loaded) return; setAreasByRegion((prev) => ({ ...prev, [regionKey]: { data: prev[regionKey]?.data || [], loading: true, loaded: false, error: null, }, })); try { const res = await apiFetch('/api/v1/areas?regionKey=' + encodeURIComponent(regionKey)); const data = Array.isArray(res) ? res : (res.data || res.areas || res.items || []); setAreasByRegion((prev) => ({ ...prev, [regionKey]: { data, loading: false, loaded: true, error: null }, })); } catch (error) { setAreasByRegion((prev) => ({ ...prev, [regionKey]: { data: prev[regionKey]?.data || [], loading: false, loaded: false, error: error.message || '区域加载失败', }, })); } }; useEffectWatch(() => { selectedKeys.forEach(ensureAreas); }, [selectedKeys.join(',')]); useEffectWatch(() => { const existing = getWatchSession(); if (!existing?.token) { setSessionLoading(false); return; } subscribeApiFetch('/me', { token: existing.token }) .then((res) => { const nextSession = { token: existing.token, user: res.user }; setSession(nextSession); setWatchSession(nextSession); }) .catch(() => { clearWatchSession(); setSession(null); }) .finally(() => setSessionLoading(false)); }, []); useEffectWatch(() => { if (!session?.token || !watchAccessEnabled) return; refreshSubscriptions(); }, [session?.token, watchAccessEnabled]); useEffectWatch(() => { if (!session?.token || !watchAccessEnabled) return; loadCatalogProperties(); }, [session?.token, watchAccessEnabled, selectedKeys.join(','), selectedAreaKeys.join(','), rent.min, rent.max]); useEffectWatch(() => { if (!session?.token || watchAccessEnabled) return; setSubscriptions([]); setCatalogProperties([]); setWatchDetailKey(null); }, [session?.token, watchAccessEnabled]); async function refreshSubscriptions() { if (!session?.token) return; setSubscriptionsLoading(true); try { const res = await subscribeApiFetch('/subscriptions', { token: session.token }); setSubscriptions(res.subscriptions || []); } catch (error) { if (error.message === 'watch_access_disabled') { updateSessionUser({ watchAccessEnabled: false }); setSubscriptions([]); return; } console.error('load subscriptions failed', error); } finally { setSubscriptionsLoading(false); } } async function loadCatalogProperties() { if (!session?.token) return; if (selectedKeys.length === 0) { setCatalogProperties([]); return; } setCatalogLoading(true); setCatalogError(''); try { const merged = []; const seen = new Set(); for (const regionKey of selectedKeys) { const res = await subscribeApiFetch('/catalog/properties?regionKey=' + encodeURIComponent(regionKey), { token: session.token, }); const list = Array.isArray(res) ? res : (res.data || res.items || []); for (const item of list) { const key = item.propertyKey || item.propertyId || item.id; if (seen.has(key)) continue; seen.add(key); merged.push(item); } } setCatalogProperties(merged); } catch (error) { if (error.message === 'watch_access_disabled') { updateSessionUser({ watchAccessEnabled: false }); setCatalogProperties([]); setCatalogError(''); return; } setCatalogError(error.message || '加载全部物业失败'); setCatalogProperties([]); } finally { setCatalogLoading(false); } } async function handleRequestCode() { setAuthLoading(true); setAuthError(''); try { const res = await subscribeApiFetch('/auth/request-code', { method: 'POST', body: { email }, }); setAuthStep('code'); setDebugCode(res.debugCode || ''); } catch (error) { setAuthError(error.message === 'invalid_email' ? '请输入正确的邮箱地址' : '验证码发送失败,请稍后重试'); } finally { setAuthLoading(false); } } async function handleVerifyCode() { setAuthLoading(true); setAuthError(''); try { const res = await subscribeApiFetch('/auth/verify-code', { method: 'POST', body: { email, code }, }); const nextSession = { token: res.token, user: res.user }; setSession(nextSession); setWatchSession(nextSession); setAuthOpen(false); setAuthStep('email'); setCode(''); setDebugCode(''); setWatchDetailKey(null); } catch (error) { setAuthError(error.message === 'invalid_code' ? '验证码不正确' : '验证失败,请稍后重试'); } finally { setAuthLoading(false); } } function handleLogout() { clearWatchSession(); setSession(null); setSubscriptions([]); } async function handleDeleteSubscription(propertyKey) { if (!session?.token) return; try { await subscribeApiFetch('/subscriptions/' + encodeURIComponent(propertyKey), { method: 'DELETE', token: session.token, }); refreshSubscriptions(); } catch (error) { if (error.message === 'watch_access_disabled') { updateSessionUser({ watchAccessEnabled: false }); return; } console.error('delete subscription failed', error); } } const saveQrCode = async () => { try { const res = await fetch(WATCH_CONTACT_QR_URL); const blob = await res.blob(); const objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = objectUrl; a.download = 'urrent-wechat-qr.jpg'; document.body.appendChild(a); a.click(); a.remove(); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1500); setSaveLabel('已开始保存'); window.setTimeout(() => setSaveLabel('保存二维码'), 1800); } catch (error) { console.error('save qr failed', error); setSaveLabel('保存失败,请重试'); window.setTimeout(() => setSaveLabel('保存二维码'), 2200); } }; const areaLabelOf = (areaKey) => { const regionKey = areaKey.split('__')[0]; const area = areasByRegion[regionKey]?.data?.find((item) => item.areaKey === areaKey); return area?.name || areaKey; }; const labelOf = (key) => { for (const group of REGIONS) { const match = group.prefs.find((pref) => pref.key === key); if (match) return match.label; } return key; }; const locLabel = selectedAreaKeys.length > 0 ? (selectedAreaKeys.length === 1 ? areaLabelOf(selectedAreaKeys[0]) : `${areaLabelOf(selectedAreaKeys[0])} 等${selectedAreaKeys.length}个片区`) : (selectedKeys.length === 1 ? labelOf(selectedKeys[0]) : '位置'); const rentLabel = (rent.min === 0 && rent.max === MAX_RENT) ? '租金' : rent.max >= MAX_RENT ? `¥${manYen(rent.min)}〜` : rent.min === 0 ? `〜¥${manYen(rent.max)}` : `¥${manYen(rent.min)}〜${manYen(rent.max)}`; const districtOptions = useMemoWatch(() => ( selectedAreaKeys.length > 0 ? buildDistrictOptions( catalogProperties, selectedAreaKeys, areasByRegion, (property) => property.districtName || property.skcs || '' ) : [] ), [catalogProperties, selectedAreaKeys, areasByRegion]); useEffectWatch(() => { const validDistricts = new Set(districtOptions.map((item) => item.key)); setSelectedDistricts((prev) => { const next = prev.filter((item) => validDistricts.has(item)); return next.length === prev.length ? prev : next; }); }, [districtOptions]); const districtLabel = selectedDistricts.length > 0 ? (selectedDistricts.length === 1 ? selectedDistricts[0] : `${selectedDistricts[0]} 等${selectedDistricts.length}个区域`) : '区域'; const filteredCatalogProperties = useMemoWatch(() => { return catalogProperties.filter((property) => { const parsed = parseRentRange(property.detailRentText || property.rent); if (parsed.max < rent.min) return false; if (parsed.min > rent.max) return false; if (selectedAreaKeys.length > 0 && !selectedAreaKeys.includes(property.areaKey)) return false; if (selectedDistricts.length > 0 && !selectedDistricts.includes(normalizeDistrictName(property.districtName || property.skcs))) return false; return true; }); }, [catalogProperties, rent.min, rent.max, selectedAreaKeys.join(','), selectedDistricts.join(',')]); if (sessionLoading) { return (
正在加载蹲房服务…
); } return (
{!session ? (
蹲房
绑定邮箱后,订阅你关心的物业名。
一旦有房源重新出现,我们会第一时间邮件通知你。
{[ { t: '物业级订阅', d: '一个账号可同时蹲多个物业名' }, { t: '邮件提醒', d: '新房源出现后直接邮件通知' }, { t: '直达房间', d: '邮件点击可直达对应房间信息' }, ].map((item) => (
{item.t}
{item.d}
))}
) : !watchAccessEnabled ? (
我的蹲房
{session.user.email}
蹲房待开通
请联系租房助理免费开启蹲房
你的邮箱已经注册成功。
联系客服后,我们会帮你开启蹲房权限。
) : (
我的蹲房
{session.user.email}
订阅中的物业
{subscriptions.length} 个订阅
{subscriptionsLoading ? (
正在加载订阅列表…
) : subscriptions.length === 0 ? (
还没有订阅任何物业,下面可以直接新增蹲房。
) : (
{subscriptions.map((item) => (
{(item.newListingCount || 0) > 0 && (
新增蹲房 {item.newListingCount}
)}
))}
)}
新增蹲房
显示该地区全部物业,选中后即可查看并订阅
{selectedAreaKeys.length > 0 && districtOptions.length > 0 && ( )}
{catalogLoading ? (
正在加载全部物业…
) : catalogError ? (
{catalogError}
) : filteredCatalogProperties.length === 0 ? (
当前筛选条件下没有找到物业。
) : (
{filteredCatalogProperties.map((property) => { const subscribed = subscriptions.some((item) => item.propertyKey === property.propertyKey); const thumbUrl = resolveImg(property.propertyImageLocalUrl || property.propertyImageRemoteUrl || property.image); return ( ); })}
)}
)} setLocSheetOpen(false)} selectedRegions={selectedKeys} selectedAreas={selectedAreaKeys} onChange={setLocationFilter} areasByRegion={areasByRegion} ensureAreas={ensureAreas} countMode="properties" /> setRentSheetOpen(false)} value={rent} onChange={setRent}/> setDistrictSheetOpen(false)} options={districtOptions} selectedDistricts={selectedDistricts} onChange={setSelectedDistricts} countMode="properties" /> {authOpen && (
setAuthOpen(false)}>
event.stopPropagation()}>
开启蹲房
请先绑定通知邮箱,并完成邮箱验证码验证。
{authStep === 'email' ? ( <> setEmail(event.target.value)} placeholder="请输入邮箱地址" style={watchStyles.input} /> {authError &&
{authError}
} ) : ( <>
验证码已发送至 {email}
setCode(event.target.value)} placeholder="请输入 6 位验证码" style={watchStyles.input} /> {debugCode &&
测试验证码:{debugCode}
} {authError &&
{authError}
} )}
)} {session && !watchAccessEnabled && (
)} {contactOpen && (
setContactOpen(false)}>
event.stopPropagation()}>
请加微信客服
微信ID {WATCH_CONTACT_WECHAT_ID}
微信客服二维码
长按或保存二维码后,使用微信扫码添加客服。
)} {watchDetailKey && ( setWatchDetailKey(null)} onSubscribed={refreshSubscriptions} onOpenRoom={onOpenRoom} /> )}
); } function WatchPropertyDetail({ propertyKey, token, subscriptions, onBack, onSubscribed, onOpenRoom }) { const [loading, setLoading] = useStateWatch(true); const [error, setError] = useStateWatch(''); const [data, setData] = useStateWatch(null); const [tab, setTab] = useStateWatch('info'); const [galleryIndex, setGalleryIndex] = useStateWatch(0); const [subscribing, setSubscribing] = useStateWatch(false); const subscribed = subscriptions.some((item) => item.propertyKey === propertyKey); useEffectWatch(() => { let cancelled = false; setLoading(true); setError(''); subscribeApiFetch('/property/' + encodeURIComponent(propertyKey) + '?markRead=1', { token }) .then((res) => { if (!cancelled) setData(res); }) .catch((err) => { if (!cancelled) setError(err.message || '加载物业信息失败'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [propertyKey, token]); const property = data?.property || {}; const activeProperty = data?.activeProperty || {}; const mergedProperty = useMemoWatch(() => { const featureTags = Array.from(new Set([...(property.featureTags || []), ...(activeProperty.featureTags || [])])); const facilityTags = Array.from(new Set([...(property.facilityTags || []), ...(activeProperty.facilityTags || [])])); const galleryImages = [ ...(property.galleryImages || []), ...(activeProperty.galleryImages || []), ]; return { ...property, ...activeProperty, galleryImages, featureTags, facilityTags, access: activeProperty.access?.length ? activeProperty.access : (property.access || []), accessText: activeProperty.accessText || property.accessText || '', detailTransportText: activeProperty.detailTransportText || property.detailTransportText || activeProperty.accessText || property.accessText || '', propertyImageLocalUrl: activeProperty.propertyImageLocalUrl || property.propertyImageLocalUrl, propertyImageRemoteUrl: activeProperty.propertyImageRemoteUrl || property.propertyImageRemoteUrl, image: activeProperty.image || property.image, address: activeProperty.address || property.address || '', householdCount: activeProperty.householdCount || property.householdCount || '', parkingInfoText: activeProperty.parkingInfoText || property.parkingInfoText || '', floorPlanText: activeProperty.floorPlanText || property.floorPlanText || '', detailRentText: activeProperty.detailRentText || property.detailRentText || property.rent || '', }; }, [property, activeProperty]); const latestListedRoom = data?.latestListedRoom || null; const rooms = activeProperty.rooms || []; const gallery = useMemoWatch(() => { 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; }, [propertyKey, mergedProperty]); async function handleSubscribe() { if (subscribed || !token) return; setSubscribing(true); try { await subscribeApiFetch('/subscriptions', { method: 'POST', token, body: { propertyKey }, }); await onSubscribed(); } catch (error) { if (error.message === 'watch_access_disabled') { onSubscribed && onSubscribed(); return; } console.error('subscribe failed', error); } finally { setSubscribing(false); } } return (
物业详情
{mergedProperty.name || propertyKey}
{loading ? (
正在加载物业信息…
) : error ? (
{error}
) : ( <>
{gallery[galleryIndex] ? ( {gallery[galleryIndex].label} ) : (
)} {gallery[galleryIndex] &&
{gallery[galleryIndex].label}
}
{gallery.length > 1 && (
{gallery.map((item, index) => ( ))}
)}
{mergedProperty.skcs || mergedProperty.districtName || mergedProperty.areaName || '—'} · {mergedProperty.regionName || '—'}
{mergedProperty.name}
{mergedProperty.address || '—'}
{tab === 'info' ? ( <>
{latestListedRoom && (
最近发现的新房间
)} {rooms.length > 0 && (
当前可见房间
{rooms.map((room) => ( ))}
)} ) : ( <>
交通信息
{mergedProperty.detailTransportText || mergedProperty.accessText || '—'}
物业信息
{(mergedProperty.featureTags || []).length > 0 && (
物件亮点
{(mergedProperty.featureTags || []).map((tag) => {tag})}
)} {(mergedProperty.facilityTags || []).length > 0 && (
配套设备
{(mergedProperty.facilityTags || []).map((tag) => {tag})}
)} {mergedProperty.parkingInfoText && (
停车信息
{mergedProperty.parkingInfoText}
)} )} )}
有房源后我们会邮件通知您
); } function InfoStat({ label, value }) { return (
{label}
{value}
); } function MetaLine({ label, value }) { return (
{label}
{value}
); } const watchStyles = { shell: { flex: 1, display: 'flex', flexDirection: 'column' }, loadingWrap: { padding: '32px 16px' }, loadingCard: { background: '#FFFFFF', borderRadius: 18, border: '1px solid #EDE6D9', padding: '20px', textAlign: 'center', color: '#6B6258', fontSize: 14 }, heroWrap: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px 20px 120px' }, heroCard: { width: '100%', background: '#FFFFFF', borderRadius: 20, padding: '28px 22px 24px', border: '1px solid #EDE6D9', boxShadow: '0 2px 10px rgba(60, 50, 30, 0.04)' }, heroRing: { width: 92, height: 92, margin: '0 auto 18px', borderRadius: 46, background: 'linear-gradient(135deg, #FBEFE6, #F4EADB)', display: 'flex', alignItems: 'center', justifyContent: 'center' }, heroIcon: { width: 64, height: 64, borderRadius: 32, background: '#FFF8F1', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #F4D9C5' }, heroTitle: { fontSize: 24, fontWeight: 700, color: '#1A1A1A', textAlign: 'center' }, heroDesc: { marginTop: 12, fontSize: 13, color: '#6B6258', lineHeight: 1.8, textAlign: 'center' }, heroFeatures: { display: 'flex', flexDirection: 'column', gap: 12, marginTop: 18, marginBottom: 20, borderTop: '1px dashed #EDE6D9', borderBottom: '1px dashed #EDE6D9', padding: '16px 0' }, heroFeature: { display: 'flex', gap: 10, alignItems: 'flex-start' }, heroDot: { width: 6, height: 6, borderRadius: 3, background: '#C6572B', marginTop: 8 }, heroFeatureTitle: { fontSize: 13, fontWeight: 600, color: '#1A1A1A' }, heroFeatureDesc: { fontSize: 11, color: '#8A7F72', marginTop: 2 }, primaryButton: { border: 'none', background: '#1A1A1A', color: '#FFFFFF', borderRadius: 14, padding: '15px 18px', fontSize: 15, fontWeight: 700, fontFamily: 'inherit', cursor: 'pointer' }, heroCtaButton: { display: 'block', width: 'fit-content', minWidth: 168, margin: '0 auto' }, ctaButton: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8 }, ctaIcon: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }, ghostButton: { border: '1px solid #E4DFD6', background: '#FFFFFF', color: '#1A1A1A', borderRadius: 999, padding: '10px 14px', fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' }, content: { flex: 1, overflowY: 'auto', padding: '14px 14px 120px' }, summaryCard: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#FFFFFF', borderRadius: 18, border: '1px solid #EDE6D9', padding: '16px 18px', marginBottom: 14 }, summaryTitle: { fontSize: 18, fontWeight: 700, color: '#1A1A1A' }, summaryEmail: { marginTop: 6, display: 'flex', alignItems: 'center', gap: 6, color: '#8A7F72', fontSize: 12, fontWeight: 500 }, pendingCard: { background: '#FFFFFF', borderRadius: 20, border: '1px solid #EDE6D9', padding: '28px 22px 24px', textAlign: 'center', boxShadow: '0 2px 10px rgba(60, 50, 30, 0.04)' }, pendingRing: { width: 92, height: 92, margin: '0 auto 18px', borderRadius: 46, background: 'linear-gradient(135deg, #FBEFE6, #F4EADB)', display: 'flex', alignItems: 'center', justifyContent: 'center' }, pendingIcon: { width: 64, height: 64, borderRadius: 32, background: '#FFF8F1', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #F4D9C5' }, pendingTitle: { fontSize: 22, fontWeight: 700, color: '#1A1A1A' }, pendingDesc: { marginTop: 10, fontSize: 16, color: '#1A1A1A', lineHeight: 1.6, fontWeight: 600 }, pendingSub: { marginTop: 10, fontSize: 13, color: '#6B6258', lineHeight: 1.8 }, sectionCard: { background: '#FFFFFF', borderRadius: 18, border: '1px solid #EDE6D9', padding: '16px', marginBottom: 14 }, sectionHead: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, marginBottom: 12 }, sectionTitle: { fontSize: 15, fontWeight: 700, color: '#1A1A1A' }, sectionSub: { fontSize: 11, color: '#8A7F72' }, emptyHint: { fontSize: 13, color: '#8A7F72', lineHeight: 1.7, padding: '4px 2px' }, errorHint: { fontSize: 13, color: '#C6572B', lineHeight: 1.7, padding: '4px 2px' }, subscriptionList: { display: 'flex', flexDirection: 'column', gap: 10 }, subscriptionRow: { display: 'flex', justifyContent: 'space-between', gap: 10, padding: '12px 0', borderTop: '1px dashed #F4EFE4' }, subscriptionMain: { background: 'transparent', border: 'none', padding: 0, textAlign: 'left', flex: 1, cursor: 'pointer', fontFamily: 'inherit' }, subscriptionName: { fontSize: 14, fontWeight: 700, color: '#1A1A1A' }, subscriptionMeta: { fontSize: 11, color: '#8A7F72', marginTop: 4 }, subscriptionRight: { display: 'flex', alignItems: 'center', gap: 8 }, newBadge: { background: '#FFF1E8', color: '#A85D37', borderRadius: 999, padding: '6px 10px', fontSize: 11, fontWeight: 700 }, deleteBtn: { width: 34, height: 34, borderRadius: 17, border: '1px solid #F1D7CB', background: '#FFF8F4', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, filterRow: { display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 12 }, filterPill: { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '9px 14px', borderRadius: 999, border: '1px solid #E4DFD6', background: '#FFFFFF', fontSize: 13, fontWeight: 500, fontFamily: 'inherit', cursor: 'pointer' }, catalogList: { display: 'flex', flexDirection: 'column', gap: 10 }, catalogItem: { display: 'flex', gap: 12, alignItems: 'center', padding: '12px', borderRadius: 16, border: '1px solid #EDE6D9', background: '#FFFCF8', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', position: 'relative' }, catalogThumb: { width: 72, height: 72, borderRadius: 12, overflow: 'hidden', background: '#F4EFE4', border: '1px solid #EDE6D9', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }, catalogThumbImg: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' }, catalogBody: { flex: 1, minWidth: 0 }, catalogName: { fontSize: 14, fontWeight: 700, color: '#1A1A1A', lineHeight: 1.4 }, catalogMeta: { fontSize: 11, color: '#8A7F72', marginTop: 4, lineHeight: 1.5 }, catalogRent: { fontSize: 12, color: '#C6572B', marginTop: 8, fontWeight: 700 }, miniBadge: { position: 'absolute', top: 10, right: 10, background: '#1A1A1A', color: '#FFFFFF', borderRadius: 999, padding: '4px 8px', fontSize: 10, fontWeight: 700 }, modalBackdrop: { position: 'fixed', inset: 0, zIndex: 140, background: 'rgba(20,18,14,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px 16px' }, modalCard: { width: '100%', maxWidth: 360, background: '#FFFCF8', borderRadius: 22, border: '1px solid #EEDFD0', boxShadow: '0 20px 48px rgba(20,18,14,0.18)', padding: '24px 18px 18px', position: 'relative' }, modalClose: { position: 'absolute', top: 14, right: 14, width: 34, height: 34, borderRadius: 17, border: 'none', background: '#F4EADB', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }, modalTitle: { fontSize: 22, fontWeight: 800, color: '#1A1A1A', lineHeight: 1.2 }, modalDesc: { fontSize: 13, color: '#6B6258', lineHeight: 1.7, marginTop: 10, marginBottom: 14 }, modalSubText: { fontSize: 12, color: '#8A7F72', marginBottom: 10 }, contactCard: { width: '100%', maxWidth: 360, background: '#FFFCF8', borderRadius: 22, border: '1px solid #EEDFD0', boxShadow: '0 20px 48px rgba(20,18,14,0.18)', padding: '24px 18px 18px', position: 'relative' }, contactTitle: { fontSize: 22, fontWeight: 800, color: '#1A1A1A', lineHeight: 1.2, textAlign: 'center' }, contactWechat: { fontSize: 15, color: '#8A7F72', fontWeight: 600, marginTop: 10, textAlign: 'center' }, contactQrWrap: { width: 220, maxWidth: '100%', margin: '18px auto 0', padding: 10, borderRadius: 20, background: '#FFFFFF', border: '1px solid #EFE3D5' }, contactQrImg: { width: '100%', height: 'auto', display: 'block', borderRadius: 14 }, contactHint: { marginTop: 12, fontSize: 12, color: '#6B6258', lineHeight: 1.7, textAlign: 'center' }, input: { width: '100%', borderRadius: 14, border: '1px solid #E4DFD6', background: '#FFFFFF', padding: '14px 16px', fontSize: 15, outline: 'none', marginBottom: 12, fontFamily: 'inherit' }, errorText: { fontSize: 12, color: '#C6572B', marginBottom: 12 }, devCode: { fontSize: 12, color: '#8A7F72', marginBottom: 12 }, detailShell: { position: 'fixed', inset: 0, background: '#FAF8F5', zIndex: 130, display: 'flex', flexDirection: 'column' }, detailTopBar: { padding: 'calc(12px + env(safe-area-inset-top, 0px)) 14px 10px', display: 'flex', alignItems: 'center', gap: 10, background: '#FAF8F5', borderBottom: '1px solid #EDE6D9' }, detailBackBtn: { width: 36, height: 36, borderRadius: 18, border: '1px solid #EDE6D9', background: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0 }, detailTopTitle: { flex: 1, minWidth: 0 }, detailTopMain: { fontSize: 14, fontWeight: 600, color: '#1A1A1A' }, detailTopSub: { fontSize: 11, color: '#8A7F72', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, detailScroll: { flex: 1, overflowY: 'auto', paddingBottom: 'calc(120px + env(safe-area-inset-bottom, 0px))' }, galleryWrap: { background: '#FFFFFF', padding: '12px 14px 14px' }, galleryMain: { width: '100%', aspectRatio: '4 / 3', borderRadius: 14, overflow: 'hidden', background: '#F4EFE4', position: 'relative' }, galleryImg: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' }, galleryEmpty: { width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }, galleryBadge: { position: 'absolute', top: 10, left: 10, padding: '4px 10px', borderRadius: 999, background: 'rgba(26,26,26,0.72)', color: '#FFFFFF', fontSize: 11, fontWeight: 500 }, galleryThumbRow: { display: 'flex', gap: 8, marginTop: 10 }, galleryThumb: { flex: '0 0 68px', height: 52, borderRadius: 8, border: '2px solid #E4DFD6', overflow: 'hidden', padding: 0, background: '#F4EFE4', cursor: 'pointer' }, galleryThumbImg: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' }, detailBlock: { padding: '16px 18px 14px', background: '#FFFFFF', borderTop: '1px solid #EDE6D9' }, detailWard: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: '#8A7F72', fontWeight: 500, marginBottom: 6 }, detailName: { fontSize: 19, fontWeight: 700, color: '#1A1A1A', lineHeight: 1.3 }, detailAddress: { marginTop: 10, fontSize: 12, color: '#6B6258', lineHeight: 1.6 }, tabRow: { display: 'flex', gap: 8, padding: '14px 14px 0' }, tabBtn: { flex: 1, padding: '11px 12px', borderRadius: 999, border: '1px solid #E4DFD6', background: '#FFFFFF', color: '#6B6258', fontSize: 13, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' }, tabBtnActive: { background: '#1A1A1A', color: '#FFFFFF', borderColor: '#1A1A1A' }, infoGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }, infoCell: { background: '#FAF6EE', borderRadius: 14, padding: '14px 16px' }, infoLabel: { fontSize: 11, color: '#8A7F72', fontWeight: 600 }, infoValue: { fontSize: 15, color: '#1A1A1A', fontWeight: 700, lineHeight: 1.5, marginTop: 6 }, latestRoomCard: { width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, border: '1px solid #EDE6D9', background: '#FFFCF8', borderRadius: 16, padding: '14px 16px', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }, latestRoomName: { fontSize: 14, fontWeight: 700, color: '#1A1A1A' }, latestRoomMeta: { fontSize: 11, color: '#8A7F72', marginTop: 4, lineHeight: 1.6 }, roomList: { display: 'flex', flexDirection: 'column', gap: 10 }, roomRow: { width: '100%', display: 'flex', justifyContent: 'space-between', gap: 10, border: '1px solid #EDE6D9', background: '#FFFCF8', borderRadius: 16, padding: '14px 16px', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }, bodyText: { fontSize: 12, color: '#1A1A1A', lineHeight: 1.8, whiteSpace: 'pre-wrap' }, metaList: { display: 'flex', flexDirection: 'column' }, metaRow: { display: 'flex', gap: 12, padding: '10px 0', borderBottom: '1px dashed #F4EFE4' }, metaLabel: { width: 64, flexShrink: 0, fontSize: 11, color: '#8A7F72', fontWeight: 600 }, metaValue: { flex: 1, fontSize: 12, color: '#1A1A1A', lineHeight: 1.7 }, tagWrap: { display: 'flex', flexWrap: 'wrap', gap: 8 }, featureTag: { padding: '8px 12px', borderRadius: 999, background: '#FFF1E8', color: '#A85D37', fontSize: 11, fontWeight: 600 }, facilityTag: { padding: '8px 12px', borderRadius: 999, background: '#F4EFE4', color: '#6B6258', fontSize: 11, fontWeight: 600 }, detailCtaBar: { position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 135, background: 'rgba(250,248,245,0.95)', borderTop: '1px solid #EDE6D9', padding: '12px 16px calc(16px + env(safe-area-inset-bottom, 0px))' }, ctaSub: { marginTop: 8, fontSize: 11, color: '#8A7F72', textAlign: 'center' }, pendingBar: { position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 120, background: 'rgba(250,248,245,0.95)', borderTop: '1px solid #EDE6D9', padding: '12px 16px calc(16px + env(safe-area-inset-bottom, 0px))' }, }; Object.assign(window, { WatchScreen });