From 30b1f5e256a8d9daba4fe5d3db1ce84ab73a46d0 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 28 Apr 2026 07:17:24 +0800 Subject: [PATCH] Surface city-card decision reasons before the paragraph City decision cards already had enough evidence, but the first screen still forced users to read the longer explanation before seeing why a card mattered. The header now caps status chips at three high-priority signals and uses product-facing labels for observed breakouts, stale METARs, AI loading, missing market prices, strong model agreement, and next-report waits. Constraint: The card should stay lightweight and avoid another explanatory section. Rejected: Keep six mixed freshness chips in the header | too much noise for first-glance scanning. Confidence: high Scope-risk: narrow Reversibility: clean Tested: npm run build Not-tested: Browser visual QA across all city states. --- CHANGELOG.md | 1 + .../components/dashboard/Dashboard.module.css | 2 +- .../scan-terminal/AiPinnedForecastView.tsx | 101 +++++++++++------- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28b9a14f..26854b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.5.5 - 2026-04-27 - 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类 +- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点 - 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效 - 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常” - 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间 diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index f070e910..0e7be769 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -11334,7 +11334,7 @@ display: flex; flex-wrap: wrap; gap: 7px; - margin-top: 10px; + margin: -2px 0 10px; } .root :global(.scan-ai-city-status-tag) { diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx index f1c35bd2..757b03e2 100644 --- a/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx +++ b/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx @@ -353,6 +353,15 @@ function AiPinnedCityCard({ String((row as { window_phase?: string | null } | null)?.window_phase || "").toLowerCase(), ), ); + const modelSpread = modelMax != null && modelMin != null ? modelMax - modelMin : null; + const modelHighlyConsistent = + modelEntries.length >= 4 && modelSpread != null && modelSpread <= 2; + const needsNextBulletin = + !observationStale && + !observedHighBreak && + !observedLowBreak && + !peakHasPassed && + (observedLowLag || paceTone === "neutral" || aiForecast.status === "loading"); const observationFreshnessLabel = buildObservationFreshnessLabel({ detail, displayTime: metarReportTimeDisplay, @@ -410,43 +419,59 @@ function AiPinnedCityCard({ const statusTags = uniqueStatusTags([ observedHighBreak ? { - label: isEn ? "Observed above models" : "实测突破模型", + label: isEn ? "Observed breakout" : "实测突破", tone: "red", } : null, - observedLowBreak - ? { - label: isEn ? "Peak revised down" : "峰值下修", - tone: "blue", - } - : null, - observedLowLag - ? { - label: isEn ? "Warm-up needs proof" : "等待升温确认", - tone: "amber", - } - : null, - observationStale - ? { - label: isEn ? "Stale observation" : "观测过旧", - tone: "amber", - } - : null, peakHasPassed ? { label: isEn ? "Peak window passed" : "峰值窗口已过", tone: "muted", } : null, - { - label: aiStatusLabel, - tone: aiStatusTone, - }, - { - label: marketFreshnessLabel, - tone: marketStatusTone, - }, - ]).slice(0, 6); + observationStale + ? { + label: isEn + ? isHkoObservation + ? "HKO stale" + : "METAR stale" + : isHkoObservation + ? "观测过旧" + : "METAR 过旧", + tone: "amber", + } + : null, + observedLowBreak + ? { + label: isEn ? "Peak revised down" : "峰值下修", + tone: "blue", + } + : null, + aiForecast.status === "loading" + ? { + label: isEn ? "AI reading" : "AI 解读中", + tone: aiStatusTone, + } + : null, + marketDecisionView.status === "unavailable" + ? { + label: isEn ? "Market missing" : "市场价格缺失", + tone: marketStatusTone, + } + : null, + modelHighlyConsistent + ? { + label: isEn ? "Models agree" : "模型高度一致", + tone: "green", + } + : null, + observedLowLag || needsNextBulletin + ? { + label: isEn ? "Wait next report" : "需要等待下一报文", + tone: "amber", + } + : null, + ]).slice(0, 3); const localizedRisksRaw = (isEn ? aiCityForecast?.risks_en : aiCityForecast?.risks_zh) || []; const localizedRisks = Array.isArray(localizedRisksRaw) @@ -475,6 +500,16 @@ function AiPinnedCityCard({ {isEn ? "Deep analysis" : "城市深度分析"}