Speed up intraday modal loading and add sync status
This commit is contained in:
@@ -2856,6 +2856,75 @@
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-strip) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-chip) {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 11px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(52, 211, 153, 0.16);
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-chip.syncing) {
|
||||
border-color: rgba(34, 211, 238, 0.2);
|
||||
background: rgba(34, 211, 238, 0.07);
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-dot) {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: #34d399;
|
||||
margin-top: 5px;
|
||||
flex: 0 0 auto;
|
||||
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.12);
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-chip.syncing .future-v2-sync-dot) {
|
||||
background: #22d3ee;
|
||||
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.12);
|
||||
animation: pulseGlow 1.25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-copy) {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-copy strong) {
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-sync-copy span) {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.root :global(.future-v2-layout) {
|
||||
display: grid;
|
||||
grid-template-columns: 360px minmax(0, 1fr);
|
||||
|
||||
@@ -592,6 +592,11 @@ export function FutureForecastModal() {
|
||||
if (!detail || !dateStr) return null;
|
||||
|
||||
const isToday = dateStr === detail.local_date;
|
||||
const detailDepth = detail.detail_depth || "full";
|
||||
const isFullDetailReady = detailDepth === "full";
|
||||
const isStructureSyncing = store.loadingState.refresh || !isFullDetailReady;
|
||||
const isMarketSyncing = store.loadingState.marketScan;
|
||||
const isAnyLayerSyncing = isStructureSyncing || isMarketSyncing;
|
||||
const view = getFutureModalView(detail, dateStr, locale);
|
||||
const scorePosition = `${50 + view.front.score / 2}%`;
|
||||
const barStyle = {
|
||||
@@ -1060,6 +1065,58 @@ export function FutureForecastModal() {
|
||||
}
|
||||
return lines.slice(0, 3);
|
||||
}, [boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView]);
|
||||
const syncStatusItems = [
|
||||
{
|
||||
key: "base",
|
||||
state: "ready",
|
||||
label:
|
||||
locale === "en-US" ? "Base analysis ready" : "基础分析已加载",
|
||||
note:
|
||||
locale === "en-US"
|
||||
? "Forecast curve, anchor state, and current structure are available."
|
||||
: "预测曲线、锚点状态和当前结构已经可用。",
|
||||
},
|
||||
{
|
||||
key: "market",
|
||||
state: isMarketSyncing ? "syncing" : "ready",
|
||||
label:
|
||||
locale === "en-US"
|
||||
? isMarketSyncing
|
||||
? "Syncing market ladder"
|
||||
: "Market ladder ready"
|
||||
: isMarketSyncing
|
||||
? "市场挂单同步中"
|
||||
: "市场挂单已加载",
|
||||
note:
|
||||
locale === "en-US"
|
||||
? isMarketSyncing
|
||||
? "Polymarket buckets and edge are updating in the background."
|
||||
: "Probability buckets and edge are in sync."
|
||||
: isMarketSyncing
|
||||
? "Polymarket 概率桶和 edge 正在后台更新。"
|
||||
: "概率桶和 edge 已同步完成。",
|
||||
},
|
||||
{
|
||||
key: "structure",
|
||||
state: isStructureSyncing ? "syncing" : "ready",
|
||||
label:
|
||||
locale === "en-US"
|
||||
? isStructureSyncing
|
||||
? "Backfilling deep structure"
|
||||
: "Deep structure ready"
|
||||
: isStructureSyncing
|
||||
? "深度结构补齐中"
|
||||
: "深度结构已加载",
|
||||
note:
|
||||
locale === "en-US"
|
||||
? isStructureSyncing
|
||||
? "Upper-air, nearby network, and deeper fusion signals are still coming in."
|
||||
: "Upper-air, nearby network, and deeper fusion signals are ready."
|
||||
: isStructureSyncing
|
||||
? "高空、周边站网和更深层融合信号还在补齐。"
|
||||
: "高空、周边站网和更深层融合信号已可用。",
|
||||
},
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1107,7 +1164,7 @@ export function FutureForecastModal() {
|
||||
<button
|
||||
className={clsx(
|
||||
"future-refresh-btn",
|
||||
store.loadingState.marketScan && "spinning",
|
||||
isAnyLayerSyncing && "spinning",
|
||||
)}
|
||||
disabled={!isPro || isProLoading}
|
||||
onClick={() => {
|
||||
@@ -1154,6 +1211,23 @@ export function FutureForecastModal() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body future-modal-body">
|
||||
<section className="future-v2-sync-strip" aria-live="polite">
|
||||
{syncStatusItems.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={clsx(
|
||||
"future-v2-sync-chip",
|
||||
item.state === "syncing" && "syncing",
|
||||
)}
|
||||
>
|
||||
<span className="future-v2-sync-dot" aria-hidden="true" />
|
||||
<div className="future-v2-sync-copy">
|
||||
<strong>{item.label}</strong>
|
||||
<span>{item.note}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
{isNoaaSettlement && (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -977,38 +977,39 @@ export function DashboardStoreProvider({
|
||||
openFutureModal: async (dateStr: string, forceRefresh = false) => {
|
||||
mapStopMotionRef.current();
|
||||
if (!selectedCity || !proAccess.subscriptionActive) return;
|
||||
const cityName = selectedCity;
|
||||
const cachedDetail = cityDetailsByName[selectedCity];
|
||||
const hasFullCachedDetail =
|
||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
||||
|
||||
setFutureModalDate(dateStr);
|
||||
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
||||
setLoadingState((current) => ({ ...current, marketScan: true }));
|
||||
if (!hasFullCachedDetail || forceRefresh) {
|
||||
setLoadingState((current) => ({
|
||||
...current,
|
||||
refresh: true,
|
||||
}));
|
||||
try {
|
||||
await ensureCityDetail(selectedCity, true, "full");
|
||||
} catch {
|
||||
} finally {
|
||||
setLoadingState((current) => ({
|
||||
...current,
|
||||
refresh: false,
|
||||
}));
|
||||
}
|
||||
void ensureCityDetail(cityName, true, "full")
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (selectedCityRef.current !== cityName) return;
|
||||
setLoadingState((current) => ({
|
||||
...current,
|
||||
refresh: false,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
setFutureModalDate(dateStr);
|
||||
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
||||
setLoadingState((current) => ({ ...current, marketScan: true }));
|
||||
void ensureCityMarketScan(
|
||||
selectedCity,
|
||||
cityName,
|
||||
forceRefresh || !marketScanByCityName[cacheKey],
|
||||
null,
|
||||
dateStr,
|
||||
)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (selectedCityRef.current !== cityName) return;
|
||||
setLoadingState((current) => ({ ...current, marketScan: false }));
|
||||
});
|
||||
},
|
||||
@@ -1019,13 +1020,16 @@ export function DashboardStoreProvider({
|
||||
}
|
||||
|
||||
mapStopMotionRef.current();
|
||||
const cachedDetail = cityDetailsByName[selectedCity];
|
||||
const cityName = selectedCity;
|
||||
const cachedDetail = cityDetailsByName[cityName];
|
||||
const hasFullCachedDetail =
|
||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
||||
if (hasFullCachedDetail && cachedDetail?.local_date) {
|
||||
setSelectedForecastDate(cachedDetail.local_date);
|
||||
setFutureModalDate(cachedDetail.local_date);
|
||||
const targetDate =
|
||||
cachedDetail?.local_date || selectedForecastDate || null;
|
||||
if (targetDate) {
|
||||
setSelectedForecastDate(targetDate);
|
||||
setFutureModalDate(targetDate);
|
||||
}
|
||||
if (!proAccess.subscriptionActive) return;
|
||||
const needsDetailRefresh =
|
||||
@@ -1038,37 +1042,39 @@ export function DashboardStoreProvider({
|
||||
refresh: needsDetailRefresh,
|
||||
marketScan: true,
|
||||
}));
|
||||
void ensureCityDetail(
|
||||
cityName,
|
||||
needsDetailRefresh,
|
||||
"full",
|
||||
)
|
||||
.then((detail) => {
|
||||
if (selectedCityRef.current !== cityName) return;
|
||||
setSelectedForecastDate(detail.local_date);
|
||||
setFutureModalDate(detail.local_date);
|
||||
|
||||
try {
|
||||
const detail = await ensureCityDetail(
|
||||
selectedCity,
|
||||
needsDetailRefresh,
|
||||
"full",
|
||||
);
|
||||
setSelectedForecastDate(detail.local_date);
|
||||
setFutureModalDate(detail.local_date);
|
||||
|
||||
const marketKey = getMarketScanCacheKey(selectedCity, detail.local_date);
|
||||
try {
|
||||
await ensureCityMarketScan(
|
||||
selectedCity,
|
||||
const marketKey = getMarketScanCacheKey(cityName, detail.local_date);
|
||||
return ensureCityMarketScan(
|
||||
cityName,
|
||||
forceRefresh || !marketScanByCityName[marketKey],
|
||||
null,
|
||||
detail.local_date,
|
||||
);
|
||||
} catch {}
|
||||
} catch {
|
||||
if (cachedDetail?.local_date) {
|
||||
setSelectedForecastDate(cachedDetail.local_date);
|
||||
setFutureModalDate(cachedDetail.local_date);
|
||||
}
|
||||
} finally {
|
||||
setLoadingState((current) => ({
|
||||
...current,
|
||||
refresh: false,
|
||||
marketScan: false,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (selectedCityRef.current !== cityName) return;
|
||||
if (cachedDetail?.local_date) {
|
||||
setSelectedForecastDate(cachedDetail.local_date);
|
||||
setFutureModalDate(cachedDetail.local_date);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (selectedCityRef.current !== cityName) return;
|
||||
setLoadingState((current) => ({
|
||||
...current,
|
||||
refresh: false,
|
||||
marketScan: false,
|
||||
}));
|
||||
});
|
||||
},
|
||||
registerMapStopMotion: (stopMotion: () => void) => {
|
||||
mapStopMotionRef.current = stopMotion;
|
||||
|
||||
Reference in New Issue
Block a user