From f5acae3c81fbebf604f55c536f07ee57903ac5b8 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 14 May 2026 02:32:56 +0800 Subject: [PATCH] feat: implement ScanTerminalDashboard with integrated monitoring, paywall, and AI-driven city analysis features --- .../dashboard/ProFeaturePaywall.tsx | 2 +- .../dashboard/ScanTerminalDashboard.tsx | 17 ++++- .../monitoring/MonitorPanel.module.css | 13 ++++ .../dashboard/monitoring/MonitorPanel.tsx | 71 ++++++++----------- 4 files changed, 59 insertions(+), 44 deletions(-) diff --git a/frontend/components/dashboard/ProFeaturePaywall.tsx b/frontend/components/dashboard/ProFeaturePaywall.tsx index 33e99c54..58a44f0a 100644 --- a/frontend/components/dashboard/ProFeaturePaywall.tsx +++ b/frontend/components/dashboard/ProFeaturePaywall.tsx @@ -14,7 +14,7 @@ const TELEGRAM_GROUP_URL = String( const SUBSCRIPTION_HELP_HREF = "/subscription-help"; type ProFeaturePaywallProps = { - feature: "today" | "history" | "future" | "assistant" | "scan"; + feature: "today" | "history" | "future" | "assistant" | "scan" | "monitor"; onClose?: () => void; }; diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 76aafe72..c230dd57 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -602,7 +602,22 @@ function ScanTerminalScreen() { renderMainView() )} {resolvedView === "monitor" && ( - + isPro ? ( + { + const matchedRow = findRowForCity(timeSortedRows, cityName); + if (matchedRow) { + setSelectedRowId(matchedRow.id); + store.preloadCityFromRow(matchedRow); + } + addAiPinnedCity(cityName); + setActiveView("analysis"); + void store.selectCity(cityName); + }} + /> + ) : ( + + ) )} diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.module.css b/frontend/components/dashboard/monitoring/MonitorPanel.module.css index 0b4318fb..9a679133 100644 --- a/frontend/components/dashboard/monitoring/MonitorPanel.module.css +++ b/frontend/components/dashboard/monitoring/MonitorPanel.module.css @@ -135,6 +135,19 @@ border-color: rgba(124, 58, 237, 0.3); } +.root :global(.monitor-card.clickable) { + cursor: pointer; +} +.root :global(.monitor-card.clickable:hover) { + border-color: var(--color-accent, #7c3aed); + transform: translateY(-2px); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18); +} +.root :global(.monitor-card.clickable:focus-visible) { + outline: 2px solid var(--color-accent, #7c3aed); + outline-offset: 2px; +} + /* ── Card header ── */ .root :global(.monitor-card-head) { display: flex; diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.tsx b/frontend/components/dashboard/monitoring/MonitorPanel.tsx index 7467cd2e..d0aefa76 100644 --- a/frontend/components/dashboard/monitoring/MonitorPanel.tsx +++ b/frontend/components/dashboard/monitoring/MonitorPanel.tsx @@ -85,40 +85,14 @@ function trendSymbol(tr: "rising" | "falling" | "flat") { } /** - * Resolve today's observed high temperature from airport METAR sources only. - * - * Priority (all METAR / airport station sources): - * 1. airport_current.max_so_far — backend rolling max (most authoritative) - * 2. max(airport_primary_today_obs) — client-derived from airport primary station's today series - * 3. max(metar_today_obs) — client-derived from METAR today obs series - * - * The settlement station (current.max_so_far) is intentionally excluded — - * it may be a different microclimate and is not the airport METAR reading. + * Resolve today's observed high temperature. + * Aligned with Telegram bot (_get_airport_daily_high): + * only airport_current.max_so_far — no fallback. */ function resolveMaxSoFar(detail: CityDetail | undefined): number | null { - // ① Backend rolling max from airport METAR — primary and most reliable - const backendMax = detail?.airport_current?.max_so_far ?? null; - if (backendMax != null) return backendMax; - - // ② Client-derived max from airport primary station's today obs series - const primaryObs = detail?.airport_primary_today_obs ?? []; - if (primaryObs.length) { - const temps = primaryObs - .map((o) => o.temp) - .filter((t): t is number => t != null); - if (temps.length) return Math.max(...temps); - } - - // ③ Client-derived max from METAR today obs series - const metarObs = detail?.metar_today_obs ?? []; - if (metarObs.length) { - const temps = metarObs - .map((o) => o.temp) - .filter((t): t is number => t != null); - if (temps.length) return Math.max(...temps); - } - - return null; + const v = detail?.airport_current?.max_so_far ?? null; + if (v == null) return null; + return Math.round(v * 10) / 10; } /* ── Airport names ───────────────────────────────────────────── */ @@ -176,7 +150,11 @@ function FreshnessDot({ level, title }: { level: Freshness; title: string }) { } /* ── Main component ──────────────────────────────────────────── */ -export default function MonitorPanel() { +export default function MonitorPanel({ + onCityClick, +}: { + onCityClick?: (cityName: string) => void; +}) { const store = useDashboardStore(); const { locale } = useI18n(); const isEn = locale === "en-US"; @@ -388,6 +366,7 @@ export default function MonitorPanel() { const obs = ac?.obs_time ?? detail.local_time ?? ""; const age = ac?.obs_age_min ?? null; const freshness = freshnessLevel(age); + const tempSymbol = detail.temp_symbol || "°C"; // °F for US cities const newHigh = cur != null && max != null && cur >= max + 0.3; const warm = !newHigh && cur != null && cur >= 30; const tr = trendClass(detail); @@ -402,7 +381,13 @@ export default function MonitorPanel() { "monitor-card", newHigh ? "new-high" : "", isRefreshing ? "refreshing" : "", + onCityClick ? "clickable" : "", ].filter(Boolean).join(" ")} + role={onCityClick ? "button" : undefined} + tabIndex={onCityClick ? 0 : undefined} + onClick={onCityClick ? () => onCityClick(key) : undefined} + onKeyDown={onCityClick ? (e) => { if (e.key === "Enter" || e.key === " ") onCityClick(key); } : undefined} + title={onCityClick ? (isEn ? `Open ${detail.display_name || key} decision card` : `打开 ${detail.display_name || key} 决策卡`) : undefined} > {/* Refresh progress bar */} {isRefreshing &&
} @@ -430,7 +415,7 @@ export default function MonitorPanel() { {cur.toFixed(1)} - °C + {tempSymbol} ) : ( -- @@ -443,7 +428,7 @@ export default function MonitorPanel() { {t("High", "高温", lang)} {max != null ? ( <> - {max.toFixed(1)}°C + {max.toFixed(1)}{tempSymbol} {mtt && {mtt}} ) : ( @@ -456,13 +441,15 @@ export default function MonitorPanel() { {t("Airport METAR", "机场报文", lang)} - {age != null - ? (isEn - ? `${age} min ago` - : age < 60 - ? `${age} 分钟未更新` - : `${Math.round(age / 60)} 小时未更新`) - : --} + {age != null ? ( + age < 60 + ? (isEn ? `${age} min ago` : `${age} 分钟未更新`) + : (isEn + ? `⚠ last ${obs || "?"}` + : `⚠ 最后报文 ${obs || "--"}`) + ) : ( + -- + )}