feat: implement ScanTerminalDashboard and LiveTemperatureThresholdChart components
This commit is contained in:
@@ -593,7 +593,7 @@ function PolyWeatherTerminal({
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#e9edf3] text-[#202833]">
|
||||
<aside
|
||||
className={clsx(
|
||||
"flex shrink-0 flex-col bg-[#11161d] py-3 text-slate-400 transition-all duration-200",
|
||||
"flex shrink-0 flex-col bg-white border-r border-[#d2d9e2] py-3 text-slate-500 transition-all duration-200",
|
||||
navExpanded ? "w-[172px] items-start px-3" : "w-[52px] items-center gap-2",
|
||||
)}
|
||||
>
|
||||
@@ -610,7 +610,7 @@ function PolyWeatherTerminal({
|
||||
<img src="/apple-touch-icon.png" alt="PolyWeather" className="h-full w-full object-cover" />
|
||||
</Link>
|
||||
{navExpanded && (
|
||||
<span className="text-sm font-black text-white tracking-tight truncate">
|
||||
<span className="text-sm font-black text-slate-800 tracking-tight truncate">
|
||||
PolyWeather
|
||||
</span>
|
||||
)}
|
||||
@@ -621,7 +621,7 @@ function PolyWeatherTerminal({
|
||||
type="button"
|
||||
onClick={() => setNavExpanded((prev) => !prev)}
|
||||
className={clsx(
|
||||
"flex items-center gap-3 transition-colors hover:text-white",
|
||||
"flex items-center gap-3 transition-colors hover:text-slate-800",
|
||||
navExpanded
|
||||
? "w-full h-8 px-1 mb-2"
|
||||
: "grid h-9 w-full place-items-center mb-2",
|
||||
@@ -630,7 +630,7 @@ function PolyWeatherTerminal({
|
||||
{navExpanded ? (
|
||||
<>
|
||||
<ChevronLeft size={14} />
|
||||
<span className="text-[11px] font-bold uppercase tracking-wide text-slate-500">
|
||||
<span className="text-[11px] font-bold uppercase tracking-wide text-slate-400">
|
||||
{isEn ? "Collapse" : "收起"}
|
||||
</span>
|
||||
</>
|
||||
@@ -654,11 +654,11 @@ function PolyWeatherTerminal({
|
||||
: "grid h-9 w-full place-items-center border-l-4",
|
||||
isActive
|
||||
? navExpanded
|
||||
? "bg-white/8 text-white"
|
||||
: "border-blue-500 bg-white/5 text-white"
|
||||
? "bg-blue-50 text-blue-600 font-bold"
|
||||
: "border-blue-500 bg-blue-50/50 text-blue-600"
|
||||
: navExpanded
|
||||
? "hover:bg-white/5 hover:text-white"
|
||||
: "border-transparent hover:bg-white/5 hover:text-white",
|
||||
? "hover:bg-slate-50 hover:text-slate-900"
|
||||
: "border-transparent hover:bg-slate-50 hover:text-slate-700",
|
||||
)}
|
||||
title={isEn ? labelEn : labelZh}
|
||||
>
|
||||
|
||||
@@ -833,18 +833,29 @@ export function LiveTemperatureThresholdChart({
|
||||
allRows = [],
|
||||
compact = false,
|
||||
onSearchClick,
|
||||
onMaximize,
|
||||
onClose,
|
||||
isMaximized = false,
|
||||
disableClose = false,
|
||||
}: {
|
||||
isEn: boolean;
|
||||
row: ScanOpportunityRow | null;
|
||||
allRows?: ScanOpportunityRow[];
|
||||
compact?: boolean;
|
||||
onSearchClick?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onClose?: () => void;
|
||||
isMaximized?: boolean;
|
||||
disableClose?: boolean;
|
||||
}) {
|
||||
const [hourly, setHourly] = useState<HourlyForecast>(null);
|
||||
const city = String(row?.city || "").toLowerCase().trim();
|
||||
const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D");
|
||||
const [hiddenSeriesKeys, setHiddenSeriesKeys] = useState<Set<string>>(new Set());
|
||||
const [prevCityAndTf, setPrevCityAndTf] = useState({ city: "", timeframe: "" });
|
||||
const [userToggledKeys, setUserToggledKeys] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
setUserToggledKeys({});
|
||||
}, [city, timeframe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!city) return;
|
||||
@@ -905,41 +916,22 @@ export function LiveTemperatureThresholdChart({
|
||||
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_")) {
|
||||
// Keep AROME HD visible by default on Paris chart
|
||||
if (city === "paris" && s.key === "model_curve_AROME HD") {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
});
|
||||
const isSeriesVisible = (sKey: string) => {
|
||||
if (userToggledKeys[sKey] !== undefined) {
|
||||
return userToggledKeys[sKey];
|
||||
}
|
||||
}, [city, timeframe, series, prevCityAndTf]);
|
||||
if (sKey.startsWith("model_curve_")) {
|
||||
if (city === "paris" && sKey === "model_curve_AROME HD") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const activeSeries = useMemo(() => {
|
||||
return chartSeries.filter((s) => !hiddenSeriesKeys.has(s.key));
|
||||
}, [chartSeries, hiddenSeriesKeys]);
|
||||
return chartSeries.filter((s) => isSeriesVisible(s.key));
|
||||
}, [chartSeries, userToggledKeys, city]);
|
||||
|
||||
const cityKey = String(row?.city || "").toLowerCase().trim();
|
||||
const runwaySensorCities = new Set([
|
||||
@@ -1067,22 +1059,61 @@ export function LiveTemperatureThresholdChart({
|
||||
);
|
||||
|
||||
const timeframeActions = (
|
||||
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
||||
{(["1D", "3D"] as const).map((tf) => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
onClick={() => setTimeframe(tf)}
|
||||
className={clsx(
|
||||
"px-2 py-0.5 text-[9px] font-bold rounded transition-all",
|
||||
timeframe === tf
|
||||
? "bg-white text-blue-600 shadow-sm border border-slate-200/50"
|
||||
: "text-slate-500 hover:text-slate-800"
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
||||
{(["1D", "3D"] as const).map((tf) => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
onClick={() => setTimeframe(tf)}
|
||||
className={clsx(
|
||||
"px-2 py-0.5 text-[9px] font-bold rounded transition-all",
|
||||
timeframe === tf
|
||||
? "bg-white text-blue-600 shadow-sm border border-slate-200/50"
|
||||
: "text-slate-500 hover:text-slate-800"
|
||||
)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(onMaximize || onClose) && (
|
||||
<div className="flex items-center gap-1">
|
||||
{onMaximize && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMaximize();
|
||||
}}
|
||||
className="grid h-6 w-6 place-items-center rounded bg-white hover:bg-slate-50 border border-slate-200 text-slate-500 hover:text-slate-800 transition-colors shadow-sm"
|
||||
title={isMaximized ? (isEn ? "Restore Grid" : "还原网格") : (isEn ? "Maximize" : "最大化")}
|
||||
>
|
||||
{isMaximized ? "❐" : "⛶"}
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disableClose}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
className={clsx(
|
||||
"grid h-6 w-6 place-items-center rounded border transition-colors shadow-sm",
|
||||
disableClose
|
||||
? "bg-slate-50 text-slate-300 border-slate-100 cursor-not-allowed"
|
||||
: "bg-white hover:bg-slate-50 border-slate-200 text-slate-500 hover:text-red-600"
|
||||
)}
|
||||
title={isEn ? "Clear Slot" : "清除槽位"}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1307,16 +1338,14 @@ export function LiveTemperatureThresholdChart({
|
||||
key={s.key}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setHiddenSeriesKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(s.key)) next.delete(s.key);
|
||||
else next.add(s.key);
|
||||
return next;
|
||||
});
|
||||
setUserToggledKeys((prev) => ({
|
||||
...prev,
|
||||
[s.key]: !isSeriesVisible(s.key),
|
||||
}));
|
||||
}}
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1.5 font-mono cursor-pointer transition-opacity hover:opacity-80",
|
||||
hiddenSeriesKeys.has(s.key) && "opacity-40 line-through"
|
||||
!isSeriesVisible(s.key) && "opacity-40 line-through"
|
||||
)}
|
||||
>
|
||||
<span className="h-2 w-2 rounded-full shrink-0" style={{ backgroundColor: s.color }} />
|
||||
|
||||
Reference in New Issue
Block a user