移除区域标签栏和自选时区面板

This commit is contained in:
2569718930@qq.com
2026-05-25 20:28:09 +08:00
parent 7f0506b897
commit 64ab74f7fc
@@ -167,11 +167,15 @@ function CityRegionList({
rows,
selectedCity,
onSelectCity,
slots = [],
activeSlotIndex = 0,
}: {
isEn: boolean;
rows: ScanOpportunityRow[];
selectedCity: string | null;
onSelectCity: (city: string) => void;
slots?: Array<string | null>;
activeSlotIndex?: number;
}) {
const cities = useMemo(() => {
const seen = new Set<string>();
@@ -191,28 +195,110 @@ function CityRegionList({
return (
<Panel title={isEn ? "Cities" : "城市"}>
<div className="divide-y divide-slate-100">
{cities.map(({ city, name, localTime }) => (
<button
key={city}
type="button"
onClick={() => onSelectCity(city)}
className={clsx(
"flex w-full items-center justify-between px-3 py-2 text-left hover:bg-blue-50/70 transition-colors",
selectedCity === city && "bg-blue-50",
)}
>
<div className="min-w-0">
<div className="text-[12px] font-bold text-slate-800 truncate">{name}</div>
<div className="text-[11px] text-slate-400">{localTime}</div>
</div>
</button>
))}
<div className="divide-y divide-slate-100 max-h-[calc(100vh-140px)] overflow-y-auto">
{cities.map(({ city, name, localTime }) => {
const isActive = selectedCity === city;
const displaySlotIndices = slots
.map((s, idx) => (s === city ? idx : -1))
.filter((idx) => idx !== -1);
return (
<button
key={city}
type="button"
onClick={() => onSelectCity(city)}
className={clsx(
"flex w-full items-center justify-between px-3 py-2 text-left hover:bg-blue-50/70 transition-colors",
isActive && "bg-blue-50 border-l-2 border-blue-500 pl-2.5",
)}
>
<div className="min-w-0 flex-1">
<div className="text-[12px] font-bold text-slate-800 truncate">{name}</div>
<div className="text-[11px] text-slate-400">{localTime}</div>
</div>
{/* Slot indicators */}
{displaySlotIndices.length > 0 && (
<div className="flex gap-0.5 ml-2">
{displaySlotIndices.map((idx) => (
<span
key={idx}
className={clsx(
"grid h-4 w-4 place-items-center rounded-full text-[9px] font-bold",
idx === activeSlotIndex
? "bg-blue-500 text-white"
: "bg-slate-200 text-slate-600"
)}
title={isEn ? `Slot ${idx + 1}` : `槽位 ${idx + 1}`}
>
{idx + 1}
</span>
))}
</div>
)}
</button>
);
})}
</div>
</Panel>
);
}
function EmptySlotCard({
slotIndex,
isActive,
isEn,
availableCities,
onSelectSlot,
onSelectCity,
}: {
slotIndex: number;
isActive: boolean;
isEn: boolean;
availableCities: { city: string; name: string }[];
onSelectSlot: () => void;
onSelectCity: (city: string) => void;
}) {
return (
<div
onClick={onSelectSlot}
className={clsx(
"flex flex-col items-center justify-center h-full rounded-[4px] border-2 border-dashed p-6 cursor-pointer bg-slate-50/50 transition-all",
isActive
? "border-blue-500 bg-blue-50/10 ring-2 ring-blue-500/20"
: "border-slate-300 hover:border-slate-400 hover:bg-slate-50/80"
)}
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-400 mb-2">
<span className="text-xl font-bold">+</span>
</div>
<div className="text-[12px] font-bold text-slate-700 mb-1">
{isEn ? `Slot ${slotIndex + 1}: Empty` : `槽位 ${slotIndex + 1}: 空白`}
</div>
<div className="text-[10px] text-slate-400 text-center mb-3 max-w-[180px]">
{isEn
? "Select this card and click a city on the left, or select below:"
: "激活此卡片并从左侧选择城市,或从下方直接选择:"}
</div>
<select
value=""
onClick={(e) => e.stopPropagation()} // prevent triggering onSelectSlot
onChange={(e) => {
if (e.target.value) onSelectCity(e.target.value);
}}
className="text-[11px] font-semibold text-slate-600 px-2 py-1 rounded border border-slate-300 bg-white outline-none max-w-[160px]"
>
<option value="">{isEn ? "Choose city..." : "选择城市..."}</option>
{availableCities.map((c) => (
<option key={c.city} value={c.city}>
{c.name}
</option>
))}
</select>
</div>
);
}
function PolyWeatherTerminal({
generatedText,
isEn,
@@ -278,6 +364,19 @@ function PolyWeatherTerminal({
const [navExpanded, setNavExpanded] = useState(false);
const [activeNavKey, setActiveNavKey] = useState<string>("contracts");
const [slots, setSlots] = useState<Array<string | null>>(() => {
try {
const stored = localStorage.getItem("polyweather_terminal_slots");
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length === 4) return parsed;
}
} catch {}
return [null, null, null, null];
});
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
const NAV_ITEMS = [
{ key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" },
{ key: "markets", Icon: Activity, labelEn: "Markets", labelZh: "市场概览" },
@@ -290,10 +389,44 @@ function PolyWeatherTerminal({
const filteredRegionRows = useMemo(() => {
return rows.filter(
(row) => getCityRegion(row) === selectedRegionKey,
(row) => resolveTradingRegionKey(row) === selectedRegionKey,
);
}, [rows, selectedRegionKey]);
useEffect(() => {
if (filteredRegionRows.length && slots.every((s) => s === null)) {
const next = [filteredRegionRows[0].city, null, null, null];
setSlots(next);
try {
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
} catch {}
}
}, [filteredRegionRows, slots]);
const handleSelectCityForSlot = (index: number, city: string | null) => {
const next = [...slots];
next[index] = city;
setSlots(next);
try {
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
} catch {}
};
const availableCities = useMemo(() => {
const seen = new Set<string>();
const result: { city: string; name: string }[] = [];
filteredRegionRows.forEach((row) => {
const key = String(row.city || "").toLowerCase();
if (seen.has(key)) return;
seen.add(key);
result.push({
city: key,
name: rowName(row),
});
});
return result;
}, [filteredRegionRows]);
const watchRows = useMemo(() => {
return filteredRegionRows
.filter((row) => decisionLabel(row) === "Watch" || !row.tradable)
@@ -502,64 +635,6 @@ function PolyWeatherTerminal({
<TrainingDashboard isEn={isEn} />
) : (
<>
{/* Region tabs */}
<div className="flex shrink-0 items-center gap-1 overflow-x-auto rounded-[4px] border border-[#cfd6df] bg-white p-1 mb-2 scrollbar-none">
{REGIONS.filter((r) => visibleRegions.has(r.key)).map((r) => ({
key: r.key,
labelEn: r.labelEn.toUpperCase(),
labelZh: r.labelZh,
})).map((tab) => (
<button
key={tab.key}
type="button"
onClick={() => setSelectedRegionKey(tab.key)}
className={clsx(
"px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider rounded-[3px] transition-all whitespace-nowrap",
selectedRegionKey === tab.key
? "bg-blue-600 text-white shadow-sm"
: "text-slate-500 hover:bg-slate-100 hover:text-slate-800"
)}
>
{isEn ? tab.labelEn : tab.labelZh}
</button>
))}
{/* Region selector gear */}
<div className="relative">
<button
type="button"
onClick={() => {
const el = document.getElementById("region-selector-popover");
if (el) el.classList.toggle("hidden");
}}
className="px-1.5 py-1 text-[10px] font-bold text-slate-400 hover:text-slate-600 rounded-[3px] hover:bg-slate-100"
title={isEn ? "Customize regions" : "自选时区"}
>
&#9881;
</button>
<div
id="region-selector-popover"
className="hidden absolute right-0 top-full mt-1 z-50 bg-white border border-slate-200 rounded-lg shadow-lg p-2 min-w-[180px]"
>
{REGIONS.map((r) => {
const checked = visibleRegions.has(r.key);
return (
<label
key={r.key}
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-slate-50 cursor-pointer text-[11px] font-semibold text-slate-700"
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleRegion(r.key)}
className="h-3.5 w-3.5"
/>
{isEn ? r.labelEn : r.labelZh}
</label>
);
})}
</div>
</div>
</div>
{/* Mobile layout */}
<div className="flex flex-col gap-2 lg:hidden overflow-auto flex-1 pb-6">
<MobileRegionTabs
@@ -607,13 +682,142 @@ function PolyWeatherTerminal({
<CityRegionList
isEn={isEn}
rows={filteredRegionRows}
selectedCity={selectedCity}
onSelectCity={setSelectedCity}
selectedCity={slots[activeSlotIndex]}
onSelectCity={(city) => handleSelectCityForSlot(activeSlotIndex, city)}
slots={slots}
activeSlotIndex={activeSlotIndex}
/>
</div>
<div className="min-h-0">
<LiveTemperatureThresholdChart isEn={isEn} row={selectedRow} allRows={filteredRegionRows} />
{maximizedSlotIndex !== null ? (
// Maximized view
<div
onClick={() => setActiveSlotIndex(maximizedSlotIndex)}
className={clsx(
"relative h-full rounded-[4px] border overflow-hidden border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10"
)}
>
{/* Floating actions toolbar */}
<div className="absolute right-[110px] top-[6px] z-20 flex items-center gap-1.5">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setMaximizedSlotIndex(null);
}}
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={isEn ? "Restore Grid" : "还原网格"}
>
</button>
<button
type="button"
disabled={slots.filter(Boolean).length <= 1}
onClick={(e) => {
e.stopPropagation();
handleSelectCityForSlot(maximizedSlotIndex, null);
setMaximizedSlotIndex(null);
}}
className={clsx(
"grid h-6 w-6 place-items-center rounded border transition-colors shadow-sm",
slots.filter(Boolean).length <= 1
? "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>
<LiveTemperatureThresholdChart
isEn={isEn}
row={filteredRegionRows.find((r) => String(r.city || "").toLowerCase() === slots[maximizedSlotIndex]) || null}
allRows={filteredRegionRows}
compact={false}
/>
</div>
) : (
// 2x2 grid layout
<div className="grid grid-cols-2 grid-rows-2 gap-2 h-full">
{[0, 1, 2, 3].map((slotIndex) => {
const isSlotActive = activeSlotIndex === slotIndex;
const cityInSlot = slots[slotIndex];
if (!cityInSlot) {
return (
<EmptySlotCard
key={slotIndex}
slotIndex={slotIndex}
isActive={isSlotActive}
isEn={isEn}
availableCities={availableCities}
onSelectSlot={() => setActiveSlotIndex(slotIndex)}
onSelectCity={(city) => handleSelectCityForSlot(slotIndex, city)}
/>
);
}
const rowForSlot = filteredRegionRows.find(
(r) => String(r.city || "").toLowerCase() === cityInSlot
) || null;
return (
<div
key={slotIndex}
onClick={() => setActiveSlotIndex(slotIndex)}
className={clsx(
"relative h-full rounded-[4px] border overflow-hidden transition-all",
isSlotActive
? "border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10"
: "border-[#d2d9e2] hover:border-slate-400"
)}
>
{/* Floating actions toolbar */}
<div className="absolute right-[110px] top-[6px] z-20 flex items-center gap-1.5">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setMaximizedSlotIndex(slotIndex);
setActiveSlotIndex(slotIndex);
}}
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={isEn ? "Maximize" : "最大化"}
>
</button>
<button
type="button"
disabled={slots.filter(Boolean).length <= 1}
onClick={(e) => {
e.stopPropagation();
handleSelectCityForSlot(slotIndex, null);
}}
className={clsx(
"grid h-6 w-6 place-items-center rounded border transition-colors shadow-sm",
slots.filter(Boolean).length <= 1
? "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>
<LiveTemperatureThresholdChart
isEn={isEn}
row={rowForSlot}
allRows={filteredRegionRows}
compact={true}
/>
</div>
);
})}
</div>
)}
</div>
</div>
</>