feat: add ProbabilityHubPage component for monitoring city risk and market scan data

This commit is contained in:
2569718930@qq.com
2026-04-22 04:04:51 +08:00
parent 4deca5c1cc
commit 5448722910
@@ -64,13 +64,40 @@ function getPositiveEdge(detail?: CityDetail | null) {
return Math.max(yesEdge, noEdge, Number.NEGATIVE_INFINITY); return Math.max(yesEdge, noEdge, Number.NEGATIVE_INFINITY);
} }
function isActiveMarketScan(detail?: CityDetail | null) {
const scan = detail?.market_scan;
if (!scan?.available) return false;
const primary = scan.primary_market as
| ({
active?: boolean | null;
closed?: boolean | null;
accepting_orders?: boolean | null;
tradable?: boolean | null;
ended_at_utc?: string | null;
} & Record<string, unknown>)
| null
| undefined;
if (!primary) return false;
if (primary.closed === true) return false;
if (primary.active === false) return false;
if (primary.accepting_orders === false) return false;
if (primary.tradable === false) return false;
if (primary.ended_at_utc) {
const endedAt = new Date(String(primary.ended_at_utc)).getTime();
if (Number.isFinite(endedAt) && endedAt <= Date.now()) {
return false;
}
}
return true;
}
function hasMarket(detail?: CityDetail | null) { function hasMarket(detail?: CityDetail | null) {
const scan = detail?.market_scan;
return Boolean( return Boolean(
detail?.market_scan?.available && isActiveMarketScan(detail) &&
((Array.isArray(detail.market_scan.all_buckets) && scan &&
detail.market_scan.all_buckets.length > 0) || ((Array.isArray(scan.all_buckets) && scan.all_buckets.length > 0) ||
(Array.isArray(detail.market_scan.top_buckets) && (Array.isArray(scan.top_buckets) && scan.top_buckets.length > 0)),
detail.market_scan.top_buckets.length > 0)),
); );
} }
@@ -138,7 +165,7 @@ function ProbabilityHubScreen() {
const results = await Promise.allSettled( const results = await Promise.allSettled(
batch.map((city) => batch.map((city) =>
dashboardClient.getCityMarketScan(city.name, { dashboardClient.getCityMarketScan(city.name, {
force: true, force: false,
targetDate: detailMap[city.name]?.local_date || null, targetDate: detailMap[city.name]?.local_date || null,
marketSlug: detailMap[city.name]?.market_scan?.selected_slug || null, marketSlug: detailMap[city.name]?.market_scan?.selected_slug || null,
}), }),
@@ -187,18 +214,22 @@ function ProbabilityHubScreen() {
setLastUpdatedAt(new Date().toISOString()); setLastUpdatedAt(new Date().toISOString());
await refreshMarketScans(fetched, cityList); await refreshMarketScans(fetched, cityList);
} catch (loadError) { } catch (loadError) {
setError( if (!Object.keys(detailsByName).length && !cities.length) {
loadError instanceof Error setError(
? loadError.message loadError instanceof Error
: locale === "en-US" ? loadError.message
? "Failed to load probability hub" : locale === "en-US"
: "加载概率页失败", ? "Failed to load probability hub"
); : "加载概率页失败",
);
} else {
console.warn("probability hub refresh failed", loadError);
}
} finally { } finally {
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
} }
}, [cities.length, fetchCityDetails, locale, refreshMarketScans]); }, [cities.length, detailsByName, fetchCityDetails, locale, refreshMarketScans]);
const retryMissingCities = useCallback(async () => { const retryMissingCities = useCallback(async () => {
if (!cities.length) return; if (!cities.length) return;
@@ -230,7 +261,7 @@ function ProbabilityHubScreen() {
useEffect(() => { useEffect(() => {
const timer = window.setInterval(() => { const timer = window.setInterval(() => {
if (document.visibilityState === "hidden") return; if (document.visibilityState === "hidden") return;
void loadAll(true); void loadAll(false);
}, FULL_REFRESH_INTERVAL_MS); }, FULL_REFRESH_INTERVAL_MS);
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
@@ -508,11 +539,18 @@ function ProbabilityHubScreen() {
</strong> </strong>
</span> </span>
</div> </div>
<ProbabilityDistribution {(() => {
detail={detail} const activeMarketScan = isActiveMarketScan(detail)
hideTitle ? detail.market_scan
marketScan={detail.market_scan} : null;
/> return (
<ProbabilityDistribution
detail={detail}
hideTitle
marketScan={activeMarketScan}
/>
);
})()}
</section> </section>
); );
})} })}