diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c520a7..4a244fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ ## 1.5.5 - 2026-04-27 +- Dashboard 新增 v1.5.5 升级公告,提示所有会员已额外延长 7 天,并集中说明 DeepSeek 机场报文解读、日历行动视图、本地时间峰值窗口和 AI 证据护栏 +- 城市决策卡空状态与市场不可用文案产品化:将“未接入/缺失”改为“市场价格暂不可用,天气判断仍可参考”,避免用户误以为系统故障 +- 城市决策卡新增“为什么推荐/为什么不推荐”短句,优先解释实测突破、峰值窗口已过、METAR 过旧、市场暂不可用或模型一致等关键原因 +- 移动端城市决策卡前置当前温度、预测高点和峰值时间;长 AI 解读在手机端默认折叠,可展开查看,市场价格单独成行展示 - 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类 -- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点 +- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价暂不可用 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点 - 城市决策卡 AI 机场报文区明确拆分“快速判断已完成,AI 正在补充机场报文细节… / AI 机场报文解读已完成 / AI 解读未完整返回,当前使用规则证据”三种状态,减少 fallback 与流式返回造成的误解 - 城市决策卡新增“数据新鲜度”区块,分别展示 METAR/官方观测、模型、市场价格和 AI 状态;过旧观测会标明“仅作背景参考” - 日历视图升级为行动视图,按“现在可看 / 1-3 小时内 / 今天稍后 / 已过峰值,等待确认”分组,并为每个城市显示一句核心原因 diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 57456eb4..0d60171c 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -9384,6 +9384,68 @@ font-variant-numeric: tabular-nums; } +.root :global(.scan-upgrade-announcement) { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + align-items: center; + margin: -4px 0 6px; + border: 1px solid rgba(77, 163, 255, 0.2); + border-radius: 18px; + background: + radial-gradient(circle at top left, rgba(77, 163, 255, 0.18), transparent 36%), + linear-gradient(135deg, rgba(18, 33, 58, 0.96), rgba(15, 23, 42, 0.92)); + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.2); + padding: 16px 18px; +} + +.root :global(.scan-upgrade-announcement-copy) { + min-width: 0; +} + +.root :global(.scan-upgrade-announcement-copy span) { + color: #86efac; + font-size: 12px; + font-weight: 900; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.root :global(.scan-upgrade-announcement-copy strong) { + display: block; + margin-top: 5px; + color: #f3f8ff; + font-size: 18px; + line-height: 1.25; +} + +.root :global(.scan-upgrade-announcement-copy p) { + margin: 6px 0 0; + color: #b4c8e2; + font-size: 13px; + line-height: 1.55; +} + +.root :global(.scan-upgrade-announcement ul) { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +.root :global(.scan-upgrade-announcement li) { + border: 1px solid rgba(77, 163, 255, 0.22); + border-radius: 999px; + background: rgba(77, 163, 255, 0.1); + color: #d7e5f7; + font-size: 12px; + font-weight: 850; + padding: 7px 10px; +} + .root :global(.scan-ghost-button), .root :global(.scan-cta-ghost), .root :global(.scan-theme-button), @@ -11314,6 +11376,10 @@ line-height: 1.1; } +.root :global(.scan-ai-city-mobile-priority) { + display: none; +} + .root :global(.scan-ai-city-pills) { display: flex; flex-wrap: wrap; @@ -11598,6 +11664,17 @@ line-height: 1.55; } +.root :global(.scan-ai-decision-why) { + display: inline-flex; + max-width: 100%; + border: 1px solid rgba(77, 163, 255, 0.24); + border-radius: 12px; + background: rgba(77, 163, 255, 0.1); + color: #d8e7f8 !important; + font-weight: 900; + padding: 9px 11px; +} + .root :global(.scan-ai-decision-reasons) { display: grid; gap: 8px; @@ -11632,6 +11709,10 @@ font-weight: 800; } +.root :global(.scan-ai-market-mobile-line) { + display: none; +} + .root :global(.scan-ai-market-decision) { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; @@ -11753,6 +11834,41 @@ margin-bottom: 12px; } +.root :global(.scan-ai-city-ai-read) { + overflow: hidden; +} + +.root :global(.scan-ai-city-ai-read summary) { + cursor: pointer; + list-style: none; +} + +.root :global(.scan-ai-city-ai-read summary::-webkit-details-marker) { + display: none; +} + +.root :global(.scan-ai-city-ai-read summary::after) { + width: 18px; + height: 18px; + display: inline-grid; + place-items: center; + border-radius: 999px; + background: rgba(77, 163, 255, 0.12); + color: #9ecbff; + content: "⌄"; + font-size: 12px; + line-height: 1; + transition: transform 0.18s ease; +} + +.root :global(.scan-ai-city-ai-read[open] summary::after) { + transform: rotate(180deg); +} + +.root :global(.scan-ai-city-section-body) { + margin-top: 2px; +} + .root :global(.scan-ai-city-section p) { margin: 0 0 10px; color: #9fb2c7; @@ -14055,6 +14171,14 @@ align-items: flex-start; } + .root :global(.scan-upgrade-announcement) { + grid-template-columns: 1fr; + } + + .root :global(.scan-upgrade-announcement ul) { + justify-content: flex-start; + } + .root :global(.scan-table-header), .root :global(.scan-table-row) { grid-template-columns: 1fr; @@ -14101,6 +14225,10 @@ grid-template-columns: 1fr; } + .root :global(.scan-ai-market-decision) { + grid-template-columns: 1fr; + } + .root :global(.scan-opportunity-card-grid), .root :global(.scan-opportunity-summary) { grid-template-columns: 1fr; @@ -14157,6 +14285,114 @@ flex-wrap: wrap; } + .root :global(.scan-upgrade-announcement) { + padding: 14px; + } + + .root :global(.scan-upgrade-announcement-copy strong) { + font-size: 16px; + } + + .root :global(.scan-upgrade-announcement ul) { + gap: 6px; + } + + .root :global(.scan-upgrade-announcement li) { + font-size: 11px; + padding: 6px 8px; + } + + .root :global(.scan-ai-city-hero) { + padding: 16px; + } + + .root :global(.scan-ai-city-hero h3) { + margin-bottom: 10px; + font-size: 24px; + } + + .root :global(.scan-ai-city-mobile-priority) { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin: 0 0 12px; + } + + .root :global(.scan-ai-city-mobile-priority span) { + display: grid; + gap: 4px; + border: 1px solid rgba(77, 163, 255, 0.18); + border-radius: 12px; + background: rgba(77, 163, 255, 0.09); + padding: 9px; + } + + .root :global(.scan-ai-city-mobile-priority small) { + color: #9fb2c7; + font-size: 10px; + font-weight: 900; + } + + .root :global(.scan-ai-city-mobile-priority b) { + color: #f3f8ff; + font-size: 13px; + line-height: 1.15; + } + + .root :global(.scan-ai-city-pills) { + display: none; + } + + .root :global(.scan-ai-city-freshness) { + grid-template-columns: 1fr; + } + + .root :global(.scan-ai-decision-band) { + padding: 14px; + } + + .root :global(.scan-ai-decision-band strong) { + font-size: 20px; + } + + .root :global(.scan-ai-decision-long), + .root :global(.scan-ai-decision-reasons), + .root :global(.scan-ai-decision-risk) { + display: none; + } + + .root :global(.scan-ai-market-mobile-line) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 12px; + border: 1px solid rgba(77, 163, 255, 0.18); + border-radius: 12px; + background: rgba(11, 18, 32, 0.48); + padding: 10px 12px; + } + + .root :global(.scan-ai-market-mobile-line span) { + color: #9fb2c7; + font-size: 12px; + font-weight: 900; + } + + .root :global(.scan-ai-market-mobile-line b) { + color: #e6edf3; + font-size: 13px; + text-align: right; + } + + .root :global(.scan-ai-market-decision) { + margin-top: 10px; + } + + .root :global(.scan-ai-city-ai-read:not([open])) { + padding-bottom: 4px; + } + .root :global(.scan-list-header) { flex-direction: column; align-items: flex-start; @@ -14321,3 +14557,40 @@ .root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.red) { color: #b91c1c !important; } + +.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement) { + background: + radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 36%), + #ffffff !important; + border-color: rgba(37, 99, 235, 0.16) !important; + box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12) !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy strong), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority b), +.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line b) { + color: #0f172a !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy p), +.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority small), +.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line span) { + color: #334155 !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy span) { + color: #047857 !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority span), +.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why), +.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line) { + background: #eef7ff !important; + border-color: rgba(37, 99, 235, 0.16) !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why) { + color: #1d4ed8 !important; +} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 8aec2dce..be2036ac 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -701,6 +701,26 @@ function ScanTerminalScreen() { +
+
+ {isEn ? "v1.5.5 upgrade" : "v1.5.5 升级公告"} + + {isEn ? "PolyWeather is now upgraded to v1.5.5" : "网站已升级到 v1.5.5"} + +

+ {isEn + ? "All members received an extra 7 days. The decision terminal now explains more evidence without making unavailable quotes look like a system failure." + : "所有会员已额外延长 7 天。新版会把证据讲得更清楚,也不会让暂无报价看起来像系统故障。"} +

+
+ +
+ ["marketStatus"]; }) { if (marketStatus === "loading") return isEn ? "syncing" : "同步中"; - if (!marketScan?.available) return isEn ? "missing" : "缺失"; + if (!marketScan?.available) return isEn ? "temporarily unavailable" : "暂不可用"; const quoteAgeMs = Number( marketScan.quote_age_ms ?? marketScan.yes_token?.quote_age_ms ?? @@ -307,6 +307,16 @@ function AiPinnedCityCard({ enabled: Boolean(detail), }); const isRefreshing = refreshingDetail || aiForecast.status === "loading"; + const [isCompactCard, setIsCompactCard] = useState(false); + + useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) return; + const media = window.matchMedia("(max-width: 820px)"); + const syncCompactMode = () => setIsCompactCard(media.matches); + syncCompactMode(); + media.addEventListener("change", syncCompactMode); + return () => media.removeEventListener("change", syncCompactMode); + }, []); const aiCityForecast = aiForecast.payload?.city_forecast || null; const localizedFinalJudgmentRaw = @@ -376,6 +386,14 @@ function AiPinnedCityCard({ peakWindow, tempSymbol, }); + const expectedHighText = + decisionExpectedHighNumber != null + ? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 }) + : "--"; + const currentTempText = + currentTempNumber != null + ? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 }) + : "--"; const marketDecisionView = buildMarketDecisionView({ expectedHigh: decisionExpectedHighNumber, isEn, @@ -540,7 +558,7 @@ function AiPinnedCityCard({ : null, marketDecisionView.status === "unavailable" ? { - label: isEn ? "Market missing" : "市场价格缺失", + label: isEn ? "Market unavailable" : "市场价暂不可用", tone: marketStatusTone, } : null, @@ -574,6 +592,43 @@ function AiPinnedCityCard({ (isEn ? aiForecast.payload?.reason_en : aiForecast.payload?.reason_zh) || aiForecast.payload?.reason || ""; + const decisionWhyText = observedHighBreak + ? isEn + ? "Worth watching now: observation has broken above the model range." + : "当前值得关注:实测已突破模型上沿。" + : peakHasPassed + ? isEn + ? "Avoid chasing now: peak window has passed; wait to confirm no new high." + : "当前不宜追高:峰值窗口已过,等待确认是否还有新高。" + : observationStale + ? isEn + ? "Use as background only: observation is stale and needs the next report." + : "当前仅作背景:观测已过旧,需要下一报文确认。" + : marketDecisionView.status === "unavailable" + ? isEn + ? "Weather evidence remains usable, but no tradable quote is available yet." + : "当前可参考天气:暂无可交易价格。" + : modelHighlyConsistent + ? isEn + ? "Worth watching now: models agree; wait for observation confirmation." + : "当前值得关注:模型高度一致,等待实测确认。" + : needsNextBulletin + ? isEn + ? "Wait for confirmation: the next bulletin should decide direction." + : "当前建议等待:下一报文更适合决定方向。" + : isEn + ? "Watch the peak window and compare observations against the expected high." + : "当前重点:盯住峰值窗口,把实测与预计高点对照。"; + const marketLineText = + marketDecisionView.status === "ready" + ? `${marketDecisionView.bucketLabel} · ${marketDecisionView.priceText}` + : marketDecisionView.status === "loading" + ? isEn + ? "syncing" + : "同步中" + : isEn + ? "temporarily unavailable" + : "暂不可用"; const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`; @@ -585,6 +640,20 @@ function AiPinnedCityCard({ {isEn ? "Deep analysis" : "城市深度分析"}

{displayName}

+
+ + {isEn ? "Observed" : "当前温度"} + {currentTempText} + + + {isEn ? "Expected high" : "预测高点"} + {expectedHighText} + + + {isEn ? "Peak" : "峰值时间"} + {peakWindow} + +
{statusTags.map((tag) => ( {isEn ? "Expected high" : "预计最高温"} - {decisionExpectedHighNumber != null - ? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 }) - : "--"} + {expectedHighText}