From 1d54f44189f5c235801b3a56cd5f6895ce8646da Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 13 Apr 2026 11:19:24 +0800 Subject: [PATCH] Speed up intraday modal loading and add sync status --- .../components/dashboard/Dashboard.module.css | 69 ++++++++++++++ .../dashboard/FutureForecastModal.tsx | 76 ++++++++++++++- frontend/hooks/useDashboardStore.tsx | 94 ++++++++++--------- 3 files changed, 194 insertions(+), 45 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 03fbe9b8..6f7db601 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -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); diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index ae54aca9..20cbb8ac 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -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 (
{ @@ -1154,6 +1211,23 @@ export function FutureForecastModal() {
+
+ {syncStatusItems.map((item) => ( +
+
+ ))} +
{isNoaaSettlement && (
{ 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;