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 (
{!session ? (
蹲房
绑定邮箱后,订阅你关心的物业名。
一旦有房源重新出现,我们会第一时间邮件通知你。
{[
{ t: '物业级订阅', d: '一个账号可同时蹲多个物业名' },
{ t: '邮件提醒', d: '新房源出现后直接邮件通知' },
{ t: '直达房间', d: '邮件点击可直达对应房间信息' },
].map((item) => (
) : !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 ? (
) : (
<>
{gallery[galleryIndex] ? (
![{gallery[galleryIndex].label}]({gallery[galleryIndex].url})
) : (
)}
{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 (
);
}
function MetaLine({ label, value }) {
return (
);
}
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 });