Speed up intraday modal loading and add sync status
This commit is contained in:
@@ -2856,6 +2856,75 @@
|
|||||||
padding-right: 14px;
|
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) {
|
.root :global(.future-v2-layout) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 360px minmax(0, 1fr);
|
grid-template-columns: 360px minmax(0, 1fr);
|
||||||
|
|||||||
@@ -592,6 +592,11 @@ export function FutureForecastModal() {
|
|||||||
if (!detail || !dateStr) return null;
|
if (!detail || !dateStr) return null;
|
||||||
|
|
||||||
const isToday = dateStr === detail.local_date;
|
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 view = getFutureModalView(detail, dateStr, locale);
|
||||||
const scorePosition = `${50 + view.front.score / 2}%`;
|
const scorePosition = `${50 + view.front.score / 2}%`;
|
||||||
const barStyle = {
|
const barStyle = {
|
||||||
@@ -1060,6 +1065,58 @@ export function FutureForecastModal() {
|
|||||||
}
|
}
|
||||||
return lines.slice(0, 3);
|
return lines.slice(0, 3);
|
||||||
}, [boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1107,7 +1164,7 @@ export function FutureForecastModal() {
|
|||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"future-refresh-btn",
|
"future-refresh-btn",
|
||||||
store.loadingState.marketScan && "spinning",
|
isAnyLayerSyncing && "spinning",
|
||||||
)}
|
)}
|
||||||
disabled={!isPro || isProLoading}
|
disabled={!isPro || isProLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1154,6 +1211,23 @@ export function FutureForecastModal() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body future-modal-body">
|
<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 && (
|
{isNoaaSettlement && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -977,38 +977,39 @@ export function DashboardStoreProvider({
|
|||||||
openFutureModal: async (dateStr: string, forceRefresh = false) => {
|
openFutureModal: async (dateStr: string, forceRefresh = false) => {
|
||||||
mapStopMotionRef.current();
|
mapStopMotionRef.current();
|
||||||
if (!selectedCity || !proAccess.subscriptionActive) return;
|
if (!selectedCity || !proAccess.subscriptionActive) return;
|
||||||
|
const cityName = selectedCity;
|
||||||
const cachedDetail = cityDetailsByName[selectedCity];
|
const cachedDetail = cityDetailsByName[selectedCity];
|
||||||
const hasFullCachedDetail =
|
const hasFullCachedDetail =
|
||||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||||
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
||||||
|
|
||||||
|
setFutureModalDate(dateStr);
|
||||||
|
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
||||||
|
setLoadingState((current) => ({ ...current, marketScan: true }));
|
||||||
if (!hasFullCachedDetail || forceRefresh) {
|
if (!hasFullCachedDetail || forceRefresh) {
|
||||||
setLoadingState((current) => ({
|
setLoadingState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
refresh: true,
|
refresh: true,
|
||||||
}));
|
}));
|
||||||
try {
|
void ensureCityDetail(cityName, true, "full")
|
||||||
await ensureCityDetail(selectedCity, true, "full");
|
.catch(() => {})
|
||||||
} catch {
|
.finally(() => {
|
||||||
} finally {
|
if (selectedCityRef.current !== cityName) return;
|
||||||
setLoadingState((current) => ({
|
setLoadingState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
refresh: false,
|
refresh: false,
|
||||||
}));
|
}));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setFutureModalDate(dateStr);
|
|
||||||
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
|
||||||
setLoadingState((current) => ({ ...current, marketScan: true }));
|
|
||||||
void ensureCityMarketScan(
|
void ensureCityMarketScan(
|
||||||
selectedCity,
|
cityName,
|
||||||
forceRefresh || !marketScanByCityName[cacheKey],
|
forceRefresh || !marketScanByCityName[cacheKey],
|
||||||
null,
|
null,
|
||||||
dateStr,
|
dateStr,
|
||||||
)
|
)
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
if (selectedCityRef.current !== cityName) return;
|
||||||
setLoadingState((current) => ({ ...current, marketScan: false }));
|
setLoadingState((current) => ({ ...current, marketScan: false }));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1019,13 +1020,16 @@ export function DashboardStoreProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapStopMotionRef.current();
|
mapStopMotionRef.current();
|
||||||
const cachedDetail = cityDetailsByName[selectedCity];
|
const cityName = selectedCity;
|
||||||
|
const cachedDetail = cityDetailsByName[cityName];
|
||||||
const hasFullCachedDetail =
|
const hasFullCachedDetail =
|
||||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||||
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
||||||
if (hasFullCachedDetail && cachedDetail?.local_date) {
|
const targetDate =
|
||||||
setSelectedForecastDate(cachedDetail.local_date);
|
cachedDetail?.local_date || selectedForecastDate || null;
|
||||||
setFutureModalDate(cachedDetail.local_date);
|
if (targetDate) {
|
||||||
|
setSelectedForecastDate(targetDate);
|
||||||
|
setFutureModalDate(targetDate);
|
||||||
}
|
}
|
||||||
if (!proAccess.subscriptionActive) return;
|
if (!proAccess.subscriptionActive) return;
|
||||||
const needsDetailRefresh =
|
const needsDetailRefresh =
|
||||||
@@ -1038,37 +1042,39 @@ export function DashboardStoreProvider({
|
|||||||
refresh: needsDetailRefresh,
|
refresh: needsDetailRefresh,
|
||||||
marketScan: true,
|
marketScan: true,
|
||||||
}));
|
}));
|
||||||
|
void ensureCityDetail(
|
||||||
|
cityName,
|
||||||
|
needsDetailRefresh,
|
||||||
|
"full",
|
||||||
|
)
|
||||||
|
.then((detail) => {
|
||||||
|
if (selectedCityRef.current !== cityName) return;
|
||||||
|
setSelectedForecastDate(detail.local_date);
|
||||||
|
setFutureModalDate(detail.local_date);
|
||||||
|
|
||||||
try {
|
const marketKey = getMarketScanCacheKey(cityName, detail.local_date);
|
||||||
const detail = await ensureCityDetail(
|
return ensureCityMarketScan(
|
||||||
selectedCity,
|
cityName,
|
||||||
needsDetailRefresh,
|
|
||||||
"full",
|
|
||||||
);
|
|
||||||
setSelectedForecastDate(detail.local_date);
|
|
||||||
setFutureModalDate(detail.local_date);
|
|
||||||
|
|
||||||
const marketKey = getMarketScanCacheKey(selectedCity, detail.local_date);
|
|
||||||
try {
|
|
||||||
await ensureCityMarketScan(
|
|
||||||
selectedCity,
|
|
||||||
forceRefresh || !marketScanByCityName[marketKey],
|
forceRefresh || !marketScanByCityName[marketKey],
|
||||||
null,
|
null,
|
||||||
detail.local_date,
|
detail.local_date,
|
||||||
);
|
);
|
||||||
} catch {}
|
})
|
||||||
} catch {
|
.catch(() => {
|
||||||
if (cachedDetail?.local_date) {
|
if (selectedCityRef.current !== cityName) return;
|
||||||
setSelectedForecastDate(cachedDetail.local_date);
|
if (cachedDetail?.local_date) {
|
||||||
setFutureModalDate(cachedDetail.local_date);
|
setSelectedForecastDate(cachedDetail.local_date);
|
||||||
}
|
setFutureModalDate(cachedDetail.local_date);
|
||||||
} finally {
|
}
|
||||||
setLoadingState((current) => ({
|
})
|
||||||
...current,
|
.finally(() => {
|
||||||
refresh: false,
|
if (selectedCityRef.current !== cityName) return;
|
||||||
marketScan: false,
|
setLoadingState((current) => ({
|
||||||
}));
|
...current,
|
||||||
}
|
refresh: false,
|
||||||
|
marketScan: false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
registerMapStopMotion: (stopMotion: () => void) => {
|
registerMapStopMotion: (stopMotion: () => void) => {
|
||||||
mapStopMotionRef.current = stopMotion;
|
mapStopMotionRef.current = stopMotion;
|
||||||
|
|||||||
Reference in New Issue
Block a user