diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index bdbc9825..e3558c5f 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -1245,9 +1245,30 @@ } .root :global(.forecast-inline-note) { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + padding: 8px 10px; + border: 1px solid rgba(34, 211, 238, 0.18); + border-radius: 8px; + color: var(--text-secondary); + background: rgba(34, 211, 238, 0.06); + font-size: 12px; line-height: 1.45; } +.root :global(.forecast-inline-note::before) { + content: ""; + width: 8px; + height: 8px; + flex: 0 0 auto; + border-radius: 999px; + background: #22d3ee; + box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.12); + animation: pulseGlow 1.25s ease-in-out infinite; +} + .root :global(.forecast-day) { background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-subtle); @@ -1287,6 +1308,28 @@ color: var(--accent-cyan); } +.root :global(.forecast-day-sync) { + cursor: wait; + opacity: 0.76; + background: + linear-gradient(90deg, rgba(15, 23, 42, 0.86), rgba(30, 41, 59, 0.72), rgba(15, 23, 42, 0.86)); + background-size: 220% 100%; + animation: panelSkeletonSweep 1.35s ease-in-out infinite; +} + +.root :global(.forecast-day-sync .f-temp) { + color: var(--text-muted); +} + +@keyframes panelSkeletonSweep { + 0% { + background-position: 120% 0; + } + 100% { + background-position: -120% 0; + } +} + @media (max-width: 720px) { .root :global(.forecast-table) { display: flex; diff --git a/frontend/components/dashboard/DetailPanel.tsx b/frontend/components/dashboard/DetailPanel.tsx index b39296b1..9c35b20f 100644 --- a/frontend/components/dashboard/DetailPanel.tsx +++ b/frontend/components/dashboard/DetailPanel.tsx @@ -190,6 +190,12 @@ export function DetailPanel() { const heroSettlementLabel = detail?.current?.settlement_source_label || basicSettlementLabel; const heroAirportLabel = detail?.risk?.airport || basicAirportLabel; + const isDetailCompleting = Boolean( + detail && + (detail.detail_depth !== "full" || + (detail.forecast?.daily?.length ?? 0) <= 1), + ); + const isPanelSyncing = store.loadingState.cityDetail || isDetailCompleting; const blurActiveElement = () => { if (typeof document === "undefined") return; @@ -309,13 +315,17 @@ export function DetailPanel() {

{panelDisplayName}

- {store.loadingState.cityDetail && ( + {isPanelSyncing && (
)} diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index 1bac3ab0..db8ffa1c 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -993,11 +993,15 @@ export function ModelForecast({ export function ForecastTable() { const store = useDashboardStore(); const { data } = useCityData(); - const { t } = useI18n(); + const { locale, t } = useI18n(); if (!data) return null; const daily = data.forecast?.daily || []; const isSparseDaily = daily.length <= 1; + const isForecastCompleting = + store.loadingState.cityDetail || + data.detail_depth !== "full" || + isSparseDaily; const resolveForecastTemp = (date: string, fallback: number | null | undefined) => { const debPrediction = data.multi_model_daily?.[date]?.deb?.prediction; return debPrediction ?? fallback ?? null; @@ -1006,17 +1010,14 @@ export function ForecastTable() {

{t("forecast.title")}

{isSparseDaily && ( -
- {store.loadingState.cityDetail - ? "多日预报同步中,正在刷新完整日序列。" - : "当前只收到当日预报,其他日期结果暂未回传。"} +
+ {isForecastCompleting + ? locale === "en-US" + ? "Multi-day forecast is syncing. Only the current-day card has arrived." + : "多日预报同步中,当前只到达当日卡片。" + : locale === "en-US" + ? "Only the current-day forecast is available right now." + : "当前只收到当日预报,其他日期结果暂未回传。"}
)}
@@ -1062,7 +1063,23 @@ export function ForecastTable() {
); - }) + }).concat( + isForecastCompleting + ? Array.from({ length: Math.max(0, 5 - daily.length) }).map((_, index) => ( + + )) + : [], + ) )}