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]">
|
<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 }} />
|
||||||
|
|||||||
Reference in New Issue
Block a user