统一 METAR 获取:非 HK/深圳城市走同一条 NOAA 路径,前端移除所有流浮山字样
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface GridLayoutSelectorProps {
|
||||
isEn: boolean;
|
||||
cols: number;
|
||||
rows: number;
|
||||
onSelectGrid: (cols: number, rows: number) => void;
|
||||
}
|
||||
|
||||
export function GridLayoutSelector({
|
||||
isEn,
|
||||
cols,
|
||||
rows,
|
||||
onSelectGrid,
|
||||
}: GridLayoutSelectorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hoveredCols, setHoveredCols] = useState(0);
|
||||
const [hoveredRows, setHoveredRows] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCellClick = (c: number, r: number) => {
|
||||
onSelectGrid(c, r);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const previewCols = hoveredCols > 0 ? hoveredCols : cols;
|
||||
const previewRows = hoveredRows > 0 ? hoveredRows : rows;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative z-30">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className="flex items-center gap-1.5 h-7 rounded border border-slate-300 bg-white px-2.5 text-[11px] font-bold text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors shadow-sm outline-none"
|
||||
title={isEn ? "Grid Layout" : "图表网格布局"}
|
||||
>
|
||||
<LayoutGrid size={13} className="text-slate-500" />
|
||||
<span>
|
||||
{isEn ? `Layout: ${cols}x${rows}` : `布局: ${cols}x${rows}`}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-8 w-[150px] bg-white border border-slate-200 rounded shadow-2xl p-3 animate-in fade-in-50 zoom-in-95 duration-100">
|
||||
<div
|
||||
className="grid grid-cols-3 gap-1 mb-2.5 mx-auto w-[90px]"
|
||||
onMouseLeave={() => {
|
||||
setHoveredCols(0);
|
||||
setHoveredRows(0);
|
||||
}}
|
||||
>
|
||||
{[1, 2, 3].map((r) =>
|
||||
[1, 2, 3].map((c) => {
|
||||
const isHighlighted = c <= previewCols && r <= previewRows;
|
||||
return (
|
||||
<div
|
||||
key={`${r}-${c}`}
|
||||
onMouseEnter={() => {
|
||||
setHoveredCols(c);
|
||||
setHoveredRows(r);
|
||||
}}
|
||||
onClick={() => handleCellClick(c, r)}
|
||||
className={clsx(
|
||||
"h-6 w-6 rounded-sm border cursor-pointer transition-colors",
|
||||
isHighlighted
|
||||
? "bg-blue-500 border-blue-600"
|
||||
: "bg-slate-100 border-slate-200 hover:bg-slate-200"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] font-bold text-center text-slate-500 font-sans tracking-wide uppercase border-t border-slate-100 pt-1.5">
|
||||
{previewCols} × {previewRows}{" "}
|
||||
{isEn
|
||||
? previewCols * previewRows === 1
|
||||
? "Chart"
|
||||
: "Charts"
|
||||
: "图表"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -842,12 +842,9 @@ export function LiveTemperatureThresholdChart({
|
||||
}) {
|
||||
const [hourly, setHourly] = useState<HourlyForecast>(null);
|
||||
const city = String(row?.city || "").toLowerCase().trim();
|
||||
const [timeframe, setTimeframe] = useState<"1D" | "3D" | "5D" | "7D">("1D");
|
||||
const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D");
|
||||
const [hiddenSeriesKeys, setHiddenSeriesKeys] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
setHiddenSeriesKeys(new Set());
|
||||
}, [timeframe]);
|
||||
const [prevCityAndTf, setPrevCityAndTf] = useState({ city: "", timeframe: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!city) return;
|
||||
@@ -892,12 +889,6 @@ export function LiveTemperatureThresholdChart({
|
||||
if (timeframe === "3D") {
|
||||
return build3DayChartData(row, hourly);
|
||||
}
|
||||
if (timeframe === "5D") {
|
||||
return buildDailyChartData(row, hourly, 5);
|
||||
}
|
||||
if (timeframe === "7D") {
|
||||
return buildDailyChartData(row, hourly, 7);
|
||||
}
|
||||
return buildFullDayChartData(row, hourly);
|
||||
}, [row, hourly, timeframe]);
|
||||
|
||||
@@ -911,11 +902,36 @@ export function LiveTemperatureThresholdChart({
|
||||
const settlementPlate = useMemo(() => runwayPlates.find((p) => p.isSettlement), [runwayPlates]);
|
||||
|
||||
const chartSeries = useMemo(() => {
|
||||
if (timeframe !== "1D") {
|
||||
return series;
|
||||
return series;
|
||||
}, [series]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentKey = `${city}-${timeframe}`;
|
||||
const prevKey = `${prevCityAndTf.city}-${prevCityAndTf.timeframe}`;
|
||||
|
||||
if (currentKey !== prevKey) {
|
||||
const defaults = new Set<string>();
|
||||
// Always default all model curves to hidden across all cities and timeframes
|
||||
series.forEach((s) => {
|
||||
if (s.key.startsWith("model_curve_")) {
|
||||
defaults.add(s.key);
|
||||
}
|
||||
});
|
||||
setHiddenSeriesKeys(defaults);
|
||||
setPrevCityAndTf({ city, timeframe });
|
||||
} else {
|
||||
setHiddenSeriesKeys((prev) => {
|
||||
const next = new Set<string>();
|
||||
const seriesKeys = new Set(series.map((s) => s.key));
|
||||
prev.forEach((k) => {
|
||||
if (seriesKeys.has(k)) {
|
||||
next.add(k);
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return series.filter((item) => !hasRunwayData || !item.key.startsWith("model_curve_"));
|
||||
}, [series, hasRunwayData, timeframe]);
|
||||
}, [city, timeframe, series, prevCityAndTf]);
|
||||
|
||||
const activeSeries = useMemo(() => {
|
||||
return chartSeries.filter((s) => !hiddenSeriesKeys.has(s.key));
|
||||
@@ -1045,7 +1061,7 @@ export function LiveTemperatureThresholdChart({
|
||||
|
||||
const timeframeActions = (
|
||||
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
||||
{(["1D", "3D", "5D", "7D"] as const).map((tf) => (
|
||||
{(["1D", "3D"] as const).map((tf) => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
@@ -1358,10 +1374,10 @@ export function LiveTemperatureThresholdChart({
|
||||
dataKey={item.key}
|
||||
name={item.label}
|
||||
stroke={item.color}
|
||||
strokeWidth={item.featured ? 2 : 1.2}
|
||||
strokeWidth={item.featured ? 2.8 : 1.2}
|
||||
strokeDasharray={item.dashed ? "4 3" : undefined}
|
||||
dot={timeframe === "5D" || timeframe === "7D"}
|
||||
activeDot={{ r: item.featured ? 5 : 4 }}
|
||||
dot={false}
|
||||
activeDot={{ r: item.featured ? 6 : 4 }}
|
||||
connectNulls={true}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user