Add map city detail panel and guard forecast modal

This commit is contained in:
2569718930@qq.com
2026-04-24 04:34:33 +08:00
parent 34594e6661
commit 1f87e19cb0
2 changed files with 148 additions and 19 deletions
@@ -34,6 +34,7 @@ import {
import { I18nProvider, useI18n } from "@/hooks/useI18n";
import { dashboardClient } from "@/lib/dashboard-client";
import type {
CityDetail,
DistributionPreviewPoint,
MarketScan,
PrimarySignal,
@@ -238,6 +239,122 @@ function buildComparisonBuckets(
];
}
function MapCityDetailPanel({ detail }: { detail: CityDetail | null }) {
const { locale } = useI18n();
const isEn = locale === "en-US";
if (!detail) {
return (
<aside className="scan-detail-panel">
<div className="scan-empty-state">
<div className="scan-empty-title">
{isEn ? "Pick a city" : "选择一个城市"}
</div>
<div className="scan-empty-copy">
{isEn ? "The right rail will show city context from the map." : "右侧会展示地图里当前城市的信息。"}
</div>
</div>
</aside>
);
}
const cityName = getLocalizedCityName(
detail.name,
detail.display_name || detail.name,
locale,
);
const tempSymbol = detail.temp_symbol || "°C";
const currentTemp = detail.current?.temp;
const dayHigh =
detail.current?.max_so_far ??
detail.airport_current?.max_so_far ??
detail.airport_primary?.max_so_far ??
null;
const airportLabel = getLocalizedAirportName(
detail.name,
detail.risk?.airport || detail.risk?.icao || "",
locale,
);
const peakLabel =
Number.isFinite(Number(detail.peak?.first_h)) && Number.isFinite(Number(detail.peak?.last_h))
? `${String(Number(detail.peak?.first_h)).padStart(2, "0")}:00-${String(
Number(detail.peak?.last_h) + 1,
).padStart(2, "0")}:00`
: "--";
return (
<aside className="scan-detail-panel">
<div className="scan-detail-header">
<div className="scan-detail-top">
<div className="scan-detail-title-wrap">
<div className="scan-detail-city-name">{cityName}</div>
<div className="scan-detail-city-sub">
{airportLabel || (isEn ? "Airport anchor pending" : "机场锚点待确认")}
</div>
</div>
</div>
</div>
<div className="scan-detail-volume-row">
<div>
<div className="scan-detail-volume-big">
{currentTemp != null
? formatTemperatureValue(currentTemp, tempSymbol)
: "--"}
</div>
<div className="scan-detail-volume-caption">
{isEn ? "Current observed temperature" : "当前实测温度"}
</div>
</div>
</div>
<section className="scan-detail-section">
<div className="scan-detail-section-title">
{isEn ? "Current Context" : "当前概况"}
</div>
<div className="scan-kv-list">
<div className="scan-kv">
<span>{isEn ? "Local Time" : "当前时间"}</span>
<strong>{detail.local_time || "--"}</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Current Temp" : "当前温度"}</span>
<strong>
{currentTemp != null
? formatTemperatureValue(currentTemp, tempSymbol)
: "--"}
</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Day High (So Far)" : "今日最高(至今)"}</span>
<strong>
{dayHigh != null
? formatTemperatureValue(dayHigh, tempSymbol)
: "--"}
</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "DEB Forecast" : "DEB 预测"}</span>
<strong>
{detail.deb?.prediction != null
? formatTemperatureValue(detail.deb.prediction, tempSymbol)
: "--"}
</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Peak Window" : "预计峰值"}</span>
<strong>{peakLabel}</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Airport" : "机场锚点"}</span>
<strong>{airportLabel || "--"}</strong>
</div>
</div>
</section>
</aside>
);
}
function DetailPanel({
row,
marketScan,
@@ -788,6 +905,8 @@ function ScanTerminalScreen() {
}, [selectedRow, detailByRowId]);
const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null;
const mapFocusedDetail = store.selectedDetail;
const mapFocusedCity = store.selectedCity;
const resolvedView: ContentView =
activeSection === "markets"
? "map"
@@ -982,11 +1101,15 @@ function ScanTerminalScreen() {
</section>
</main>
<DetailPanel
row={selectedRow}
marketScan={selectedDetail}
loading={detailLoadingId === selectedRow?.id}
/>
{resolvedView === "map" && mapFocusedCity ? (
<MapCityDetailPanel detail={mapFocusedDetail} />
) : (
<DetailPanel
row={selectedRow}
marketScan={selectedDetail}
loading={detailLoadingId === selectedRow?.id}
/>
)}
</div>
</div>
);