feat: implement ScanTerminalDashboard and LiveTemperatureThresholdChart components

This commit is contained in:
2569718930@qq.com
2026-05-25 22:06:54 +08:00
parent 368388899d
commit de8e92c985
2 changed files with 93 additions and 64 deletions
@@ -593,7 +593,7 @@ function PolyWeatherTerminal({
<div className="flex h-screen w-full overflow-hidden bg-[#e9edf3] text-[#202833]"> <div className="flex h-screen w-full overflow-hidden bg-[#e9edf3] text-[#202833]">
<aside <aside
className={clsx( 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", 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" /> <img src="/apple-touch-icon.png" alt="PolyWeather" className="h-full w-full object-cover" />
</Link> </Link>
{navExpanded && ( {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 PolyWeather
</span> </span>
)} )}
@@ -621,7 +621,7 @@ function PolyWeatherTerminal({
type="button" type="button"
onClick={() => setNavExpanded((prev) => !prev)} onClick={() => setNavExpanded((prev) => !prev)}
className={clsx( className={clsx(
"flex items-center gap-3 transition-colors hover:text-white", "flex items-center gap-3 transition-colors hover:text-slate-800",
navExpanded navExpanded
? "w-full h-8 px-1 mb-2" ? "w-full h-8 px-1 mb-2"
: "grid h-9 w-full place-items-center mb-2", : "grid h-9 w-full place-items-center mb-2",
@@ -630,7 +630,7 @@ function PolyWeatherTerminal({
{navExpanded ? ( {navExpanded ? (
<> <>
<ChevronLeft size={14} /> <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" : "收起"} {isEn ? "Collapse" : "收起"}
</span> </span>
</> </>
@@ -654,11 +654,11 @@ function PolyWeatherTerminal({
: "grid h-9 w-full place-items-center border-l-4", : "grid h-9 w-full place-items-center border-l-4",
isActive isActive
? navExpanded ? navExpanded
? "bg-white/8 text-white" ? "bg-blue-50 text-blue-600 font-bold"
: "border-blue-500 bg-white/5 text-white" : "border-blue-500 bg-blue-50/50 text-blue-600"
: navExpanded : navExpanded
? "hover:bg-white/5 hover:text-white" ? "hover:bg-slate-50 hover:text-slate-900"
: "border-transparent hover:bg-white/5 hover:text-white", : "border-transparent hover:bg-slate-50 hover:text-slate-700",
)} )}
title={isEn ? labelEn : labelZh} title={isEn ? labelEn : labelZh}
> >
@@ -833,18 +833,29 @@ export function LiveTemperatureThresholdChart({
allRows = [], allRows = [],
compact = false, compact = false,
onSearchClick, onSearchClick,
onMaximize,
onClose,
isMaximized = false,
disableClose = false,
}: { }: {
isEn: boolean; isEn: boolean;
row: ScanOpportunityRow | null; row: ScanOpportunityRow | null;
allRows?: ScanOpportunityRow[]; allRows?: ScanOpportunityRow[];
compact?: boolean; compact?: boolean;
onSearchClick?: () => void; onSearchClick?: () => void;
onMaximize?: () => void;
onClose?: () => void;
isMaximized?: boolean;
disableClose?: boolean;
}) { }) {
const [hourly, setHourly] = useState<HourlyForecast>(null); const [hourly, setHourly] = useState<HourlyForecast>(null);
const city = String(row?.city || "").toLowerCase().trim(); const city = String(row?.city || "").toLowerCase().trim();
const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D"); const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D");
const [hiddenSeriesKeys, setHiddenSeriesKeys] = useState<Set<string>>(new Set()); const [userToggledKeys, setUserToggledKeys] = useState<Record<string, boolean>>({});
const [prevCityAndTf, setPrevCityAndTf] = useState({ city: "", timeframe: "" });
useEffect(() => {
setUserToggledKeys({});
}, [city, timeframe]);
useEffect(() => { useEffect(() => {
if (!city) return; if (!city) return;
@@ -905,41 +916,22 @@ export function LiveTemperatureThresholdChart({
return series; return series;
}, [series]); }, [series]);
useEffect(() => { const isSeriesVisible = (sKey: string) => {
const currentKey = `${city}-${timeframe}`; if (userToggledKeys[sKey] !== undefined) {
const prevKey = `${prevCityAndTf.city}-${prevCityAndTf.timeframe}`; return userToggledKeys[sKey];
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;
});
} }
}, [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(() => { const activeSeries = useMemo(() => {
return chartSeries.filter((s) => !hiddenSeriesKeys.has(s.key)); return chartSeries.filter((s) => isSeriesVisible(s.key));
}, [chartSeries, hiddenSeriesKeys]); }, [chartSeries, userToggledKeys, city]);
const cityKey = String(row?.city || "").toLowerCase().trim(); const cityKey = String(row?.city || "").toLowerCase().trim();
const runwaySensorCities = new Set([ const runwaySensorCities = new Set([
@@ -1067,22 +1059,61 @@ export function LiveTemperatureThresholdChart({
); );
const timeframeActions = ( const timeframeActions = (
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200"> <div className="flex items-center gap-1.5">
{(["1D", "3D"] as const).map((tf) => ( <div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
<button {(["1D", "3D"] as const).map((tf) => (
key={tf} <button
type="button" key={tf}
onClick={() => setTimeframe(tf)} type="button"
className={clsx( onClick={() => setTimeframe(tf)}
"px-2 py-0.5 text-[9px] font-bold rounded transition-all", className={clsx(
timeframe === tf "px-2 py-0.5 text-[9px] font-bold rounded transition-all",
? "bg-white text-blue-600 shadow-sm border border-slate-200/50" timeframe === tf
: "text-slate-500 hover:text-slate-800" ? "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>
)} )}
> {onClose && (
{tf} <button
</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> </div>
); );
@@ -1307,16 +1338,14 @@ export function LiveTemperatureThresholdChart({
key={s.key} key={s.key}
type="button" type="button"
onClick={() => { onClick={() => {
setHiddenSeriesKeys((prev) => { setUserToggledKeys((prev) => ({
const next = new Set(prev); ...prev,
if (next.has(s.key)) next.delete(s.key); [s.key]: !isSeriesVisible(s.key),
else next.add(s.key); }));
return next;
});
}} }}
className={clsx( className={clsx(
"inline-flex items-center gap-1.5 font-mono cursor-pointer transition-opacity hover:opacity-80", "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 }} /> <span className="h-2 w-2 rounded-full shrink-0" style={{ backgroundColor: s.color }} />