React + Mapbox GL: Как уложить пользовательские элементы управления в правом верхнем углу без перекрытия?CSS

Разбираемся в CSS
Ответить
Anonymous
 React + Mapbox GL: Как уложить пользовательские элементы управления в правом верхнем углу без перекрытия?

Сообщение Anonymous »

hello,
Я строю логистическую панель с помощью React, `React-map-gl` и Tailwind CSS. Я пытаюсь разместить два компонента управления в верхнем правом углу моей карты: < /p>
1. Пользовательский компонент `mapControls Стандартный компонент `navigationControl` (для Zoom in/out).
Я хочу, чтобы` navigationControl` был сложен вертикально, непосредственно под кнопкой `mapControls`
Проблема
. CSS с `! Важный '), два компонента всегда перекрываются друг с другом. Я также попытался дать компоненту `mapControls 'более высокий` Z-Index', чтобы убедиться, что его выпадающий список появляется сверху. Я даже попытался принудить макет с помощью внешней таблицы стилей, но перекрытие сохраняется. Logisticspage.ts (основной компонент)

Код: Выделить всё

import React, { useEffect, useState, useRef, useMemo, memo } from "react";
import { getVehicles } from "@/services/api.ts";
import {
Truck,
Battery,
Thermometer,
Box,
Search,
X,
GaugeCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { Vehicle, VehicleStatus } from "@/types";

// Map Imports
import Map, {
Marker,
Popup,
Source,
Layer,
NavigationControl, // 1.  Yahan NavigationControl import karein
} from "react-map-gl";
import type { MapRef, ViewStateChangeEvent } from "react-map-gl";
import useSupercluster from "use-supercluster";
import GeocoderControl from "@/components/GeocoderControl";
import { MapControls, mapStyles } from "@/components/MapControls";
import type { MapStyle } from "@/components/MapControls";

// CSS Imports
import "mapbox-gl/dist/mapbox-gl.css";
import "@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css";

// --- Helper Components (UI Enhanced) ---

const VehicleStatusBadge = memo(({ status }: { status: VehicleStatus }) => {
const statusConfig: Record<
VehicleStatus,
{ className: string; icon: React.ReactNode }
> = {
"On Route": {
className: "bg-cyan-500/10 text-cyan-400 border border-cyan-500/20",
icon: (

),
},
Idle: {
className: "bg-zinc-700 text-zinc-300 border border-zinc-600/50",
icon: ,
},
"In-Shop": {
className: "bg-amber-500/10 text-amber-400 border border-amber-500/20",
icon: ,
},
};

return (

{statusConfig[status].icon}
{status}

);
});

const SkeletonLoader = memo(() => (

{[...Array(6)].map((_, i) => (







))}

));

const EmptyState = memo(() => (


No Vehicles Found

Try adjusting your search or filter criteria.

));

const DetailCard = memo(
({
icon,
label,
value,
children,
}: {
icon: React.ReactNode;
label: string;
value?: string | number;
children?: React.ReactNode;
}) => (

{icon}

{label}
{value && 
{value}
}
{children}


)
);

// --- MAIN LOGISTICS PAGE COMPONENT ---
const LogisticsPage: React.FC = () =>  {
const [vehicles, setVehicles] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedVehicle, setSelectedVehicle] = useState(null);
const [popupInfo, setPopupInfo] = useState(null);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState(
"All"
);
const [viewState, setViewState] = useState({
longitude: 73.8567, // Pune
latitude: 18.5204,
zoom: 10,
pitch: 30,
bearing: 0,
});

// State for new MapControls
const [currentMapStyle, setCurrentMapStyle] = useState("Default");
const [showTraffic, setShowTraffic] = useState(false);
const [show3D, setShow3D] = useState(false);
const [showPublicTransport, setShowPublicTransport] = useState(false);
const [showBicycling, setShowBicycling] = useState(false);
const [showStreetView, setShowStreetView] = useState(false);
const [showWildfires, setShowWildfires] = useState(false);
const [showAirQuality, setShowAirQuality] = useState(false);

const mapRef = useRef(null);
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || "";

const fetchVehicles = async () => {
try {
const response = await getVehicles();
setVehicles(response.data);
if (response.data.length > 0 && !selectedVehicle) {
const firstVehicle = response.data[0];
setSelectedVehicle(firstVehicle);
setViewState((prev) => ({
...prev,
longitude: firstVehicle.longitude,
latitude: firstVehicle.latitude,
zoom: 12,
}));
}
} catch (error) {
console.error("Failed to fetch vehicles:", error);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchVehicles();
const interval = setInterval(fetchVehicles, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);

const handleVehicleSelect = (vehicle: Vehicle) => {
setSelectedVehicle(vehicle);
setPopupInfo(vehicle);
mapRef.current?.flyTo({
center: [vehicle.longitude, vehicle.latitude],
duration: 2000,
zoom: 14,
pitch: 45,
});
};

const filteredVehicles = useMemo(() => {
return vehicles
.filter((v) => statusFilter === "All" || v.status === statusFilter)
.filter(
(v) =>
v.vehicle_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
v.driver_name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [vehicles, searchTerm, statusFilter]);

const points = useMemo(
() =>
filteredVehicles.map((vehicle) => ({
type: "Feature",
properties: { cluster: false, vehicleId: vehicle.id, vehicle },
geometry: {
type: "Point",
coordinates: [vehicle.longitude, vehicle.latitude],
},
})),
[filteredVehicles]
);

const bounds = mapRef.current
? (mapRef.current.getMap().getBounds().toArray().flat() as [
number,
number,
number,
number
])
: undefined;

const { clusters, supercluster } = useSupercluster({
points,
bounds,
zoom: viewState.zoom,
options: { radius: 75, maxZoom: 20 },
});

const getStatusColor = (status: VehicleStatus) => {
switch (status) {
case "On Route":
return "#22d3ee";
case "In-Shop":
return "#f59e0b";
default:
return "#6b7280";
}
};

// Logic to add/remove 3D buildings layer
useEffect(() =>  {
const map = mapRef.current?.getMap();
if (!map) return;
const buildingsLayerId = "3d-buildings";
if (show3D) {
if (!map.getLayer(buildingsLayerId)) {
map.addLayer({
id: buildingsLayerId,
source: "composite",
"source-layer": "building",
filter: ["==", "extrude", "true"],
type: "fill-extrusion",
minzoom: 15,
paint: {
"fill-extrusion-color": "#aaa",
"fill-extrusion-height": ["get", "height"],
"fill-extrusion-base": ["get", "min_height"],
"fill-extrusion-opacity": 0.6,
},
});
}
} else {
if (map.getLayer(buildingsLayerId)) {
map.removeLayer(buildingsLayerId);
}
}
}, [show3D]);

return (


{!MAPBOX_TOKEN ? (


Map cannot be displayed.
Please add your{" "}

VITE_MAPBOX_ACCESS_TOKEN
{" "}
to the .env file.

) : (
setViewState(evt.viewState)}
style={{ width: "100%", height: "100%" }}
mapStyle={mapStyles[currentMapStyle]}
mapboxAccessToken={MAPBOX_TOKEN}
projection={{ name: "globe" }}
>
{/* --- Controls Header --- */}

{/* Left Side: Geocoder */}



{/* Right Side: Map Controls */}

setShowTraffic(!showTraffic)}
show3D={show3D}
on3DToggle={() => setShow3D(!show3D)}
showPublicTransport={showPublicTransport}
onPublicTransportToggle={() =>
setShowPublicTransport(!showPublicTransport)
}
showBicycling={showBicycling}
onBicyclingToggle={() => setShowBicycling(!showBicycling)}
showStreetView={showStreetView}
onStreetViewToggle={() => setShowStreetView(!showStreetView)}
showWildfires={showWildfires}
onWildfiresToggle={() => setShowWildfires(!showWildfires)}
showAirQuality={showAirQuality}
onAirQualityToggle={() => setShowAirQuality(!showAirQuality)}
className="relative [&>.absolute]:right-0"
/>



{/* 2. Zoom Controls ko yahan add karein */}
className="absolute top-4 right-4 z-10"
style={{ top: "70px" }}
>


{/* --- Map Layers, Markers, etc. --- */}
{showTraffic && (



)}

{clusters.map((cluster) => {
const [longitude, latitude] = cluster.geometry.coordinates;
const { cluster: isCluster, point_count: pointCount } =
cluster.properties;

if (isCluster) {
return (

className="w-10 h-10 bg-cyan-600 bg-opacity-80 backdrop-blur-sm rounded-full flex items-center justify-center text-white font-bold cursor-pointer border-2 border-cyan-400/50"
onClick={() => {
const expansionZoom = Math.min(
supercluster.getClusterExpansionZoom(
cluster.id as number
),
20
);
mapRef.current?.flyTo({
center: [longitude, latitude],
zoom: expansionZoom,
duration: 800,
});
}}
>
{pointCount}


);
}

const vehicle = cluster.properties.vehicle as Vehicle;
const isSelected = selectedVehicle?.id === vehicle.id;
return (

className="cursor-pointer group relative"
onClick={() => handleVehicleSelect(vehicle)}
>
{isSelected && (

)}



);
})}

{popupInfo && (
longitude={popupInfo.longitude}
latitude={popupInfo.latitude}
onClose={() => setPopupInfo(null)}
closeOnClick={false}
anchor="bottom"
offset={40}
className="mapbox-popup-dark"
>

{popupInfo.vehicle_number}

Driver: {popupInfo.driver_name}


)}

)}


{/* --- Sidebar Area (No changes here) --- */}


Vehicle Fleet



setSearchTerm(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:ring-2 focus:ring-cyan-500 focus:outline-none"
/>

{(["All", "On Route", "Idle", "In-Shop"] as const).map(
(status) => (
setStatusFilter(status)}
className={cn(
"px-3 py-1.5 text-sm font-medium rounded-full",
statusFilter === status
? "bg-cyan-500 text-white shadow-[0_0_10px_rgba(34,211,238,0.4)]"
: "bg-zinc-800 text-zinc-300 hover:bg-zinc-700"
)}
>
{status}

)
)}




{loading ? (



) : filteredVehicles.length === 0 ? (

) : (

{filteredVehicles.map((vehicle) => (
handleVehicleSelect(vehicle)}
className={cn(
"p-4 rounded-lg cursor-pointer transition-all border-2",
selectedVehicle?.id === vehicle.id
? "bg-zinc-800 border-cyan-500 shadow-[0_0_15px_rgba(34,211,238,0.2)]"
: "bg-zinc-800/50 border-transparent hover:bg-zinc-800"
)}
>


{vehicle.vehicle_number}



{vehicle.driver_name}

))}

)}


{selectedVehicle && (


Vehicle Details
setSelectedVehicle(null)}
className="p-1.5 rounded-full hover:bg-zinc-700"
aria-label="Close details"
>





{selectedVehicle.driver_name}











)}


);
};

export default LogisticsPage;

< /code>
2. MapControls.tsx (пользовательский компонент управления)

Код: Выделить всё

import React, { useState, useRef, useEffect } from "react";
import {
Layers,
Car,
Building,
Bus,
Bike,
Navigation,
Flame,
Wind,
} from "lucide-react";
import { cn } from "@/lib/utils";

// Types
export const mapStyles = {
Default: "mapbox://styles/mapbox/dark-v11",
Satellite: "mapbox://styles/mapbox/satellite-streets-v12",
Terrain: "mapbox://styles/mapbox/outdoors-v12",
};
export type MapStyle = keyof typeof mapStyles;

interface MapControlsProps {
currentStyle: MapStyle;
onStyleChange: (style: MapStyle) => void;
showTraffic: boolean;
onTrafficToggle: () => void;
show3D: boolean;
on3DToggle: () => void;
showPublicTransport: boolean;
onPublicTransportToggle: () => void;
showBicycling: boolean;
onBicyclingToggle: () => void;
showStreetView: boolean;
onStreetViewToggle: () => void;
showWildfires: boolean;
onWildfiresToggle: () => void;
showAirQuality: boolean;
onAirQualityToggle: () => void;
className?: string;
}

// Custom hook to detect clicks outside a component
function useOnClickOutside(
ref: React.RefObject,
handler: () => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler();
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}

export const MapControls: React.FC = ({
currentStyle,
onStyleChange,
showTraffic,
onTrafficToggle,
show3D,
on3DToggle,
showPublicTransport,
onPublicTransportToggle,
showBicycling,
onBicyclingToggle,
showStreetView,
onStreetViewToggle,
showWildfires,
onWildfiresToggle,
showAirQuality,
onAirQualityToggle,
className,
}) => {
const [isOpen, setIsOpen] = useState(false);
const controlRef = useRef(null);

// Close panel on click outside
useOnClickOutside(controlRef, () => setIsOpen(false));

const mapDetails = [
{
id: "traffic",
label: "Traffic",
icon: Car,
state: showTraffic,
action: onTrafficToggle,
},
{
id: "3d",
label: "3D Buildings",
icon: Building,
state: show3D,
action: on3DToggle,
},
{
id: "public",
label: "Public Transport",
icon: Bus,
state: showPublicTransport,
action: onPublicTransportToggle,
},
{
id: "bike",
label: "Bicycling",
icon: Bike,
state: showBicycling,
action: onBicyclingToggle,
},
{
id: "street",
label: "Street View",
icon: Navigation,
state: showStreetView,
action: onStreetViewToggle,
},
{
id: "wildfire",
label: "Wildfires",
icon: Flame,
state: showWildfires,
action: onWildfiresToggle,
},
{
id: "air",
label: "Air Quality",
icon: Wind,
state: showAirQuality,
action: onAirQualityToggle,
},
];

return (

 setIsOpen(!isOpen)}
className="bg-zinc-900/80 backdrop-blur-md text-white p-2.5 rounded-lg shadow-lg border border-zinc-700/50 hover:bg-zinc-800 transition-colors"
aria-label="Toggle map layers"
>



{isOpen &&  (


[h4]
Map Type
[/h4]

{(Object.keys(mapStyles) as MapStyle[]).map((style) => (
 onStyleChange(style)}
className={cn(
"px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
currentStyle === style
? "bg-cyan-500 text-white shadow-[0_0_10px_rgba(34,211,238,0.4)]"
: "bg-zinc-800 text-zinc-300 hover:bg-zinc-700"
)}
>
{style}

))}



[h4]
Map Details
[/h4]
{/* --- SCROLLABLE AREA --- */}

{mapDetails.map((detail) => (



{detail.label}





))}



)}

);
};
< /code>
 Информация отладки < /h3>
Я проверил элемент в инструментах разработчика браузера. Этот скриншот показывает структуру HTML и прикладные стили CSS < /p>
Введите описание изображения здесь < /p>
Введите описание изображения здесь < /p>
 Мой вопрос < /h3>
Каков правильный способ структурировать мой jsx и css для достижения желаемого версического стека? Я подозреваю, что между внутренней позицией моего пользовательского компонента есть глубокий конфликт CSS: Absolute 
и контейнер для макета, но я не могу найти основную причину.
Любая помощь будет очень оценена!>

Подробнее здесь: https://stackoverflow.com/questions/797 ... er-without
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «CSS»