From e766728c5f213c4703e4f5e961585b9fa38f8463 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 23 Apr 2026 00:57:49 +0800 Subject: [PATCH] Refine homepage focus panel --- frontend/components/dashboard/CitySidebar.tsx | 8 +- .../components/dashboard/Dashboard.module.css | 201 +++++++++++++++ .../dashboard/PolyWeatherDashboard.tsx | 242 ++++++++++++++++-- frontend/lib/dashboard-home-copy.ts | 164 ++++++++++++ 4 files changed, 597 insertions(+), 18 deletions(-) create mode 100644 frontend/lib/dashboard-home-copy.ts diff --git a/frontend/components/dashboard/CitySidebar.tsx b/frontend/components/dashboard/CitySidebar.tsx index 34ee1219..5f1e9e5d 100644 --- a/frontend/components/dashboard/CitySidebar.tsx +++ b/frontend/components/dashboard/CitySidebar.tsx @@ -5,6 +5,7 @@ import clsx from "clsx"; import { Clock } from "lucide-react"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; +import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; import { CityListItem, DeviationMonitor } from "@/lib/dashboard-types"; type RiskGroupKey = "high" | "medium" | "low" | "other"; @@ -211,6 +212,11 @@ export function CitySidebar() { const detail = store.cityDetailsByName[city.name]; const summary = store.citySummariesByName[city.name]; const snapshot = detail || summary; + const localizedCityName = getLocalizedCityName( + city.name, + snapshot?.display_name || city.display_name, + locale, + ); const isActive = store.selectedCity === city.name; const tempSymbol = snapshot?.temp_symbol || "°C"; const currentTempText = @@ -258,7 +264,7 @@ export function CitySidebar() {
- {city.display_name} + {localizedCityName} @@ -202,6 +209,139 @@ function buildSparklinePoints(values: number[] | undefined) { .join(" "); } +type HomeWeatherIconKind = + | "clear" + | "partly" + | "cloudy" + | "rain" + | "storm" + | "mist" + | "wind"; + +type HomeTrendChart = { + forecastPath: string; + legendText: string; + observationDots: Array<{ cx: number; cy: number; key: string }>; + tickLabels: Array<{ key: string; label: string; x: number }>; +}; + +function projectHomeTrendPoint( + x: number, + y: number, + xMin: number, + xMax: number, + yMin: number, + yMax: number, +) { + const width = 296; + const height = 78; + const left = 10; + const right = 10; + const top = 8; + const bottom = 12; + const plotWidth = width - left - right; + const plotHeight = height - top - bottom; + const normalizedX = + xMax === xMin ? 0.5 : Math.min(1, Math.max(0, (x - xMin) / (xMax - xMin))); + const normalizedY = + yMax === yMin ? 0.5 : Math.min(1, Math.max(0, (y - yMin) / (yMax - yMin))); + return { + cx: Number((left + normalizedX * plotWidth).toFixed(1)), + cy: Number((top + (1 - normalizedY) * plotHeight).toFixed(1)), + }; +} + +function getHomeWeatherIconKind(detail?: CityDetail | null, locale = "zh-CN"): HomeWeatherIconKind { + if (!detail) return "cloudy"; + const summary = getWeatherSummary(detail, locale === "en-US" ? "en-US" : "zh-CN"); + const weatherText = `${summary.weatherIcon} ${summary.weatherText} ${detail.current?.wx_desc || ""} ${detail.current?.cloud_desc || ""}`.toLowerCase(); + const cloudCover = Number(detail.hourly_next_48h?.cloud_cover?.[0]); + const windSpeed = Number(detail.current?.wind_speed_kt ?? detail.airport_current?.wind_speed_kt); + + if (/⛈|雷|storm|thunder/.test(weatherText)) return "storm"; + if (/🌧|🌦|雨|drizzle|shower|rain/.test(weatherText)) return "rain"; + if (/🌫|雾|mist|fog|haze/.test(weatherText)) return "mist"; + if (/💨|飑|squall/.test(weatherText) || windSpeed >= 22) return "wind"; + if (/☀|晴|clear|sunny/.test(weatherText)) return "clear"; + if (/🌤|⛅|partly|few|scattered|少云|散云/.test(weatherText)) return "partly"; + if (/☁|云|cloud|overcast|阴/.test(weatherText)) return "cloudy"; + if (Number.isFinite(cloudCover) && cloudCover <= 15) return "clear"; + if (Number.isFinite(cloudCover) && cloudCover <= 55) return "partly"; + return "cloudy"; +} + +function buildHomeTrendChart( + detail?: CityDetail | null, + locale = "zh-CN", +): HomeTrendChart | null { + if (!detail) return null; + const chartData = getTemperatureChartData(detail, locale === "en-US" ? "en-US" : "zh-CN"); + if (!chartData) return null; + const forecastSeries = chartData.datasets.hasMgmHourly + ? chartData.datasets.mgmHourlySeries + : [...chartData.datasets.debPastSeries, ...chartData.datasets.debFutureSeries]; + const observationSeries = + chartData.datasets.metarSeries.length > 0 + ? chartData.datasets.metarSeries + : chartData.datasets.airportMetarSeries; + if (!forecastSeries.length && !observationSeries.length) return null; + + const forecastPath = forecastSeries + .map((point) => { + const projected = projectHomeTrendPoint( + point.x, + point.y, + chartData.xMin, + chartData.xMax, + chartData.min, + chartData.max, + ); + return `${projected.cx},${projected.cy}`; + }) + .join(" "); + const observationDots = observationSeries.map((point, index) => { + const projected = projectHomeTrendPoint( + point.x, + point.y, + chartData.xMin, + chartData.xMax, + chartData.min, + chartData.max, + ); + return { + cx: projected.cx, + cy: projected.cy, + key: `${point.labelTime}-${index}`, + }; + }); + const tickLabels = chartData.tickLabels + .map((label, index) => { + if (!label) return null; + const minutes = Number.parseInt(String(chartData.times[index] || "0").split(":")[0] || "0", 10) * 60; + const projected = projectHomeTrendPoint( + minutes, + chartData.min, + chartData.xMin, + chartData.xMax, + chartData.min, + chartData.max, + ); + return { + key: `${label}-${index}`, + label, + x: projected.cx, + }; + }) + .filter((item): item is { key: string; label: string; x: number } => item != null); + + return { + forecastPath, + legendText: chartData.legendText, + observationDots, + tickLabels, + }; +} + function readNumericField(source: unknown, key: string) { if (!source || typeof source !== "object") return undefined; const value = (source as Record)[key]; @@ -347,13 +487,31 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) { const isLoading = store.loadingState.cityDetail && store.selectedCity === city.name; const isPro = store.proAccess.subscriptionActive; const cityCode = city.icao || detail?.risk?.icao || city.airport; - const subtitle = `${cityCode} · ${city.airport}`; + const localizedCityName = getLocalizedCityDisplay(city, locale, summary, detail); + const localizedAirportName = getLocalizedAirportDisplay(city, locale, detail); + const subtitle = `${cityCode} · ${localizedAirportName}`; const highRiskLabel = riskLevel === "high" ? locale === "en-US" ? "High risk" : "高风险" : getRiskCopy(riskLevel, locale); + const weatherIconKind = getHomeWeatherIconKind(detail, locale); + const trendChart = buildHomeTrendChart(detail, locale); + const debLabel = locale === "en-US" ? "DEB forecast" : "DEB 预测"; + const dayMaxLabel = locale === "en-US" ? "24h max" : "24 小时最高"; + const probabilityTitle = locale === "en-US" ? "EMOS probability" : "EMOS 概率"; + const marketTitle = locale === "en-US" ? "Market edge" : "市场优势"; + const marketEdgeLabel = locale === "en-US" ? "Edge" : "优势"; + const marketImpliedLabel = locale === "en-US" ? "Implied" : "市场隐含"; + const marketModelLabel = locale === "en-US" ? "Model prob" : "模型概率"; + const proLabel = isPro + ? locale === "en-US" + ? "Pro signal" + : "PRO 信号" + : locale === "en-US" + ? "Pro locked" + : "PRO 锁定"; const keySignals = [ { active: Number(marketEdge) > 0, @@ -381,7 +539,7 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) { ]; return ( -