diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index ae0f8380..bdbc9825 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -3183,12 +3183,59 @@ } .root :global(.future-modal-body) { + position: relative; display: flex; flex-direction: column; gap: 14px; padding-right: 14px; } +.root :global(.future-modal-body-refreshing > :not(.future-v2-refresh-lock)) { + opacity: 0.18; + filter: blur(1px); + pointer-events: none; + user-select: none; +} + +.root :global(.future-v2-refresh-lock) { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 16px; + border: 1px solid rgba(34, 211, 238, 0.34); + border-radius: 8px; + background: rgba(8, 13, 24, 0.96); + box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34); +} + +.root :global(.future-v2-refresh-lock strong) { + display: block; + color: #e0faff; + font-size: 14px; + font-weight: 900; +} + +.root :global(.future-v2-refresh-lock p) { + margin: 5px 0 0; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.55; +} + +.root :global(.future-v2-refresh-spinner) { + width: 18px; + height: 18px; + flex: 0 0 auto; + margin-top: 1px; + border: 2px solid rgba(34, 211, 238, 0.18); + border-top-color: #22d3ee; + border-radius: 999px; + animation: spin 0.8s linear infinite; +} + .root :global(.future-v2-sync-strip) { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index 52a44d71..1b651ad3 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -724,6 +724,7 @@ export function FutureForecastModal() { const isFullDetailReady = detailDepth === "full"; const isStructureSyncing = store.loadingState.futureDeep || !isFullDetailReady; const isAnyLayerSyncing = isStructureSyncing; + const isTodayBlockingRefresh = isToday && isStructureSyncing; const view = getFutureModalView(detail, dateStr, locale); const scorePosition = `${50 + view.front.score / 2}%`; const barStyle = { @@ -1261,25 +1262,41 @@ export function FutureForecastModal() { const syncStatusItems = [ { key: "base", - state: "ready", + state: isAnyLayerSyncing ? "syncing" : "ready", label: - locale === "en-US" ? "Base analysis ready" : "基础分析已加载", + isAnyLayerSyncing + ? locale === "en-US" + ? "Refreshing base analysis" + : "正在刷新基础分析" + : locale === "en-US" ? "Base analysis ready" : "基础分析已加载", note: - locale === "en-US" - ? "Forecast curve, anchor state, and the core intraday view are available." - : "预测曲线、锚点状态和核心日内视图已经可用。", + isAnyLayerSyncing + ? locale === "en-US" + ? "Latest anchor readings and forecast curve are being rebuilt." + : "正在重建最新锚点读数和预测曲线。" + : locale === "en-US" + ? "Forecast curve, anchor state, and the core intraday view are available." + : "预测曲线、锚点状态和核心日内视图已经可用。", }, { key: "market", - state: "ready", + state: isAnyLayerSyncing ? "syncing" : "ready", label: - locale === "en-US" - ? "Probability layer ready" - : "概率层已加载", + isAnyLayerSyncing + ? locale === "en-US" + ? "Refreshing probability layer" + : "正在刷新概率层" + : locale === "en-US" + ? "Probability layer ready" + : "概率层已加载", note: - locale === "en-US" - ? "Probability buckets are derived from the local model stack." - : "概率桶当前由本地模型栈推导。", + isAnyLayerSyncing + ? locale === "en-US" + ? "Model spread and calibrated buckets are updating." + : "模型分歧和校准概率桶正在更新。" + : locale === "en-US" + ? "Probability buckets are derived from the local model stack." + : "概率桶当前由本地模型栈推导。", }, ] as const; @@ -1391,7 +1408,29 @@ export function FutureForecastModal() { × -
+
+ {isTodayBlockingRefresh && ( +
+
+ )} {isToday && (
@@ -1453,7 +1492,10 @@ export function FutureForecastModal() { {syncStatusItems.map((item) => (