feat: Add checkbox for sightbar
entity + fix build errors
This commit is contained in:
@ -13,7 +13,6 @@ import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource, { VectorSourceEvent } from "ol/source/Vector";
|
||||
import { Draw, Modify, Select } from "ol/interaction";
|
||||
import { DrawEvent } from "ol/interaction/Draw";
|
||||
import { ModifyEvent } from "ol/interaction/Modify";
|
||||
import { SelectEvent } from "ol/interaction/Select";
|
||||
import {
|
||||
Style,
|
||||
@ -34,7 +33,6 @@ import {
|
||||
Landmark,
|
||||
Pencil,
|
||||
Save,
|
||||
Plus,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
@ -382,7 +380,8 @@ class MapService {
|
||||
|
||||
this.modifyInteraction = new Modify({
|
||||
source: this.vectorSource,
|
||||
style: (feature) => {
|
||||
// @ts-ignore
|
||||
style: (feature: FeatureLike) => {
|
||||
const originalFeature = feature.get("features")[0];
|
||||
if (
|
||||
originalFeature &&
|
||||
@ -475,7 +474,7 @@ class MapService {
|
||||
);
|
||||
});
|
||||
|
||||
this.modifyInteraction.on("modifyend", (event: ModifyEvent) => {
|
||||
this.modifyInteraction.on("modifyend", () => {
|
||||
if (this.beforeModifyState) {
|
||||
this.addStateToHistory("modify-before", this.beforeModifyState);
|
||||
this.beforeModifyState = null;
|
||||
@ -976,7 +975,54 @@ class MapService {
|
||||
}
|
||||
}
|
||||
|
||||
// --- ИСПРАВЛЕННЫЙ МЕТОД ---
|
||||
// --- НОВОЕ ---
|
||||
// Метод для множественного удаления объектов по их ID
|
||||
public deleteMultipleFeatures(featureIds: (string | number)[]): void {
|
||||
if (!featureIds || featureIds.length === 0) return;
|
||||
|
||||
// Вывод в консоль по требованию
|
||||
console.log("Запрос на множественное удаление. ID объектов:", featureIds);
|
||||
|
||||
const currentState = this.getCurrentStateAsGeoJSON();
|
||||
if (currentState) {
|
||||
this.addStateToHistory("multiple delete", currentState);
|
||||
}
|
||||
|
||||
const selectedFeaturesCollection = this.selectInteraction?.getFeatures();
|
||||
let deletedCount = 0;
|
||||
|
||||
featureIds.forEach((id) => {
|
||||
const feature = this.vectorSource.getFeatureById(id);
|
||||
if (feature) {
|
||||
// Удаление из "бэкенда"/стора для каждого объекта
|
||||
const recourse = String(id).split("-")[0];
|
||||
const numericId = String(id).split("-")[1];
|
||||
if (recourse && numericId) {
|
||||
mapStore.deleteRecourse(recourse, Number(numericId));
|
||||
}
|
||||
|
||||
// Если удаляемый объект выбран для редактирования, убираем его из выделения
|
||||
if (selectedFeaturesCollection?.getArray().includes(feature)) {
|
||||
selectedFeaturesCollection.remove(feature);
|
||||
}
|
||||
|
||||
// Удаляем объект с карты
|
||||
this.vectorSource.removeFeature(feature);
|
||||
deletedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (deletedCount > 0) {
|
||||
// Если основное выделение стало пустым, оповещаем React
|
||||
if (selectedFeaturesCollection?.getLength() === 0) {
|
||||
this.onFeatureSelect(null);
|
||||
}
|
||||
toast.success(`Удалено ${deletedCount} объект(ов).`);
|
||||
} else {
|
||||
toast.warn("Не найдено объектов для удаления.");
|
||||
}
|
||||
}
|
||||
|
||||
public getAllFeaturesAsGeoJSON(): string | null {
|
||||
if (!this.vectorSource || !this.map) return null;
|
||||
const feats = this.vectorSource.getFeatures();
|
||||
@ -984,11 +1030,9 @@ class MapService {
|
||||
|
||||
const geoJSONFmt = new GeoJSON();
|
||||
|
||||
// Просто передаем опции трансформации в метод writeFeatures.
|
||||
// Он сам всё сделает правильно, не трогая оригинальные объекты.
|
||||
return geoJSONFmt.writeFeatures(feats, {
|
||||
dataProjection: "EPSG:4326", // В какую проекцию конвертировать (стандарт для GeoJSON)
|
||||
featureProjection: this.map.getView().getProjection(), // В какой проекции находятся объекты на карте
|
||||
dataProjection: "EPSG:4326",
|
||||
featureProjection: this.map.getView().getProjection(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -1111,6 +1155,11 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
const [activeSection, setActiveSection] = useState<string | null>("layers");
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// --- НОВОЕ ---
|
||||
// Состояние для хранения ID объектов, выбранных для удаления
|
||||
const [selectedForDeletion, setSelectedForDeletion] = useState<
|
||||
Set<string | number>
|
||||
>(new Set());
|
||||
|
||||
const toggleSection = (id: string) =>
|
||||
setActiveSection(activeSection === id ? null : id);
|
||||
@ -1135,6 +1184,40 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
[mapService]
|
||||
);
|
||||
|
||||
// --- НОВОЕ ---
|
||||
// Обработчик изменения состояния чекбокса
|
||||
const handleCheckboxChange = useCallback(
|
||||
(id: string | number | undefined) => {
|
||||
if (id === undefined) return;
|
||||
setSelectedForDeletion((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// --- НОВОЕ ---
|
||||
// Обработчик для запуска множественного удаления
|
||||
const handleBulkDelete = useCallback(() => {
|
||||
if (!mapService || selectedForDeletion.size === 0) return;
|
||||
|
||||
if (
|
||||
window.confirm(
|
||||
`Вы уверены, что хотите удалить ${selectedForDeletion.size} объект(ов)? Это действие нельзя отменить.`
|
||||
)
|
||||
) {
|
||||
const idsToDelete = Array.from(selectedForDeletion);
|
||||
mapService.deleteMultipleFeatures(idsToDelete);
|
||||
setSelectedForDeletion(new Set()); // Очищаем выбор после удаления
|
||||
}
|
||||
}, [mapService, selectedForDeletion]);
|
||||
|
||||
const handleEditFeature = useCallback(
|
||||
(featureType: string | undefined, fullId: string | number | undefined) => {
|
||||
if (!featureType || !fullId) return;
|
||||
@ -1191,18 +1274,35 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
const sId = s.getId();
|
||||
const sName = (s.get("name") as string) || "Без названия";
|
||||
const isSelected = selectedFeature?.getId() === sId;
|
||||
// --- ИЗМЕНЕНИЕ ---
|
||||
const isCheckedForDeletion =
|
||||
sId !== undefined && selectedForDeletion.has(sId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={String(sId)}
|
||||
className={`flex items-start justify-between p-2 rounded-md cursor-pointer group transition-colors duration-150 ${
|
||||
className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${
|
||||
isSelected
|
||||
? "bg-orange-100 border border-orange-300 hover:bg-orange-200"
|
||||
? "bg-orange-100 border border-orange-300"
|
||||
: "hover:bg-blue-50"
|
||||
}`}
|
||||
onClick={() => handleFeatureClick(sId)}
|
||||
>
|
||||
<div className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0">
|
||||
{/* --- НОВОЕ: Чекбокс для множественного выбора --- */}
|
||||
<div className="flex-shrink-0 pr-2 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||
checked={!!isCheckedForDeletion}
|
||||
onChange={() => handleCheckboxChange(sId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`Выбрать ${sName} для удаления`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer"
|
||||
onClick={() => handleFeatureClick(sId)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<MapPin
|
||||
size={16}
|
||||
@ -1266,6 +1366,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
const lId = l.getId();
|
||||
const lName = (l.get("name") as string) || "Без названия";
|
||||
const isSelected = selectedFeature?.getId() === lId;
|
||||
const isCheckedForDeletion =
|
||||
lId !== undefined && selectedForDeletion.has(lId);
|
||||
const lGeom = l.getGeometry();
|
||||
let lineLengthText: string | null = null;
|
||||
if (lGeom instanceof LineString) {
|
||||
@ -1276,14 +1378,26 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={String(lId)}
|
||||
className={`flex items-start justify-between p-2 rounded-md cursor-pointer group transition-colors duration-150 ${
|
||||
className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${
|
||||
isSelected
|
||||
? "bg-orange-100 border border-orange-300 hover:bg-orange-200"
|
||||
? "bg-orange-100 border border-orange-300"
|
||||
: "hover:bg-blue-50"
|
||||
}`}
|
||||
onClick={() => handleFeatureClick(lId)}
|
||||
>
|
||||
<div className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0">
|
||||
<div className="flex-shrink-0 pr-2 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||
checked={!!isCheckedForDeletion}
|
||||
onChange={() => handleCheckboxChange(lId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`Выбрать ${lName} для удаления`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer"
|
||||
onClick={() => handleFeatureClick(lId)}
|
||||
>
|
||||
<div className="flex items-center mb-0.5">
|
||||
<ArrowRightLeft
|
||||
size={16}
|
||||
@ -1352,17 +1466,31 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
const sId = s.getId();
|
||||
const sName = (s.get("name") as string) || "Без названия";
|
||||
const isSelected = selectedFeature?.getId() === sId;
|
||||
const isCheckedForDeletion =
|
||||
sId !== undefined && selectedForDeletion.has(sId);
|
||||
return (
|
||||
<div
|
||||
key={String(sId)}
|
||||
className={`flex items-center justify-between p-2 rounded-md cursor-pointer group transition-colors duration-150 ${
|
||||
className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${
|
||||
isSelected
|
||||
? "bg-orange-100 border border-orange-300 hover:bg-orange-200"
|
||||
? "bg-orange-100 border border-orange-300"
|
||||
: "hover:bg-blue-50"
|
||||
}`}
|
||||
onClick={() => handleFeatureClick(sId)}
|
||||
>
|
||||
<div className="flex items-center text-gray-800 text-sm flex-grow mr-2 min-w-0">
|
||||
<div className="flex-shrink-0 pr-2 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||
checked={!!isCheckedForDeletion}
|
||||
onChange={() => handleCheckboxChange(sId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`Выбрать ${sName} для удаления`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer"
|
||||
onClick={() => handleFeatureClick(sId)}
|
||||
>
|
||||
<Landmark
|
||||
size={16}
|
||||
className={`mr-1.5 flex-shrink-0 ${
|
||||
@ -1427,12 +1555,13 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
// --- ИЗМЕНЕНИЕ: Реструктуризация для футера с кнопками ---
|
||||
<div className="w-72 relative md:w-80 bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]">
|
||||
<div className="p-4 bg-gray-700 text-white">
|
||||
<h2 className="text-lg font-semibold">Панель управления</h2>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto max-h-[70%]">
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{sections.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
@ -1480,18 +1609,30 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="m-3 w-[90%] h-[40px] flex items-center justify-center px-4 py-2 bg-blue-500 disabled:bg-blue-300 text-white rounded-md hover:bg-blue-600 transition-colors"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
{isLoading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
"Сохранить изменения"
|
||||
{/* --- НОВОЕ: Футер сайдбара с кнопками действий --- */}
|
||||
<div className="p-3 border-t border-gray-200 bg-gray-50/95 space-y-2">
|
||||
{selectedForDeletion.size > 0 && (
|
||||
<button
|
||||
onClick={handleBulkDelete}
|
||||
className="w-full flex items-center justify-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
<Trash2 size={16} className="mr-2" />
|
||||
Удалить выбранное ({selectedForDeletion.size})
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="w-full h-[40px] flex items-center justify-center px-4 py-2 bg-blue-500 disabled:bg-blue-300 text-white rounded-md hover:bg-blue-600 transition-colors"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
{isLoading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
"Сохранить изменения"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1502,9 +1643,8 @@ export const MapPage: React.FC = () => {
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const [mapServiceInstance, setMapServiceInstance] =
|
||||
useState<MapService | null>(null);
|
||||
// --- ИЗМЕНЕНИЕ: Разделение состояния загрузки ---
|
||||
const [isMapLoading, setIsMapLoading] = useState<boolean>(true); // Для рендеринга карты
|
||||
const [isDataLoading, setIsDataLoading] = useState<boolean>(true); // Для загрузки данных с API
|
||||
const [isMapLoading, setIsMapLoading] = useState<boolean>(true);
|
||||
const [isDataLoading, setIsDataLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentMapMode, setCurrentMapMode] = useState<string>("edit");
|
||||
const [mapFeatures, setMapFeatures] = useState<Feature<Geometry>[]>([]);
|
||||
@ -1525,12 +1665,10 @@ export const MapPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
let service: MapService | null = null;
|
||||
if (mapRef.current && tooltipRef.current && !mapServiceInstance) {
|
||||
// Изначально оба процесса загрузки активны
|
||||
setIsMapLoading(true);
|
||||
setIsDataLoading(true);
|
||||
setError(null);
|
||||
|
||||
// --- ИЗМЕНЕНИЕ: Логика загрузки данных вынесена и управляет своим состоянием ---
|
||||
const loadInitialData = async (mapService: MapService) => {
|
||||
console.log("Starting data load...");
|
||||
try {
|
||||
@ -1548,7 +1686,6 @@ export const MapPage: React.FC = () => {
|
||||
console.error("Failed to load initial map data:", e);
|
||||
setError("Не удалось загрузить данные для карты.");
|
||||
} finally {
|
||||
// Завершаем состояние загрузки данных независимо от результата
|
||||
setIsDataLoading(false);
|
||||
}
|
||||
};
|
||||
@ -1556,7 +1693,7 @@ export const MapPage: React.FC = () => {
|
||||
try {
|
||||
service = new MapService(
|
||||
{ ...mapConfig, target: mapRef.current },
|
||||
setIsMapLoading, // MapService теперь управляет только состоянием загрузки карты
|
||||
setIsMapLoading,
|
||||
setError,
|
||||
setCurrentMapMode,
|
||||
handleFeaturesChange,
|
||||
@ -1565,7 +1702,6 @@ export const MapPage: React.FC = () => {
|
||||
);
|
||||
setMapServiceInstance(service);
|
||||
|
||||
// Запускаем загрузку данных
|
||||
loadInitialData(service);
|
||||
} catch (e: any) {
|
||||
console.error("MapPage useEffect error:", e);
|
||||
@ -1574,7 +1710,6 @@ export const MapPage: React.FC = () => {
|
||||
e.message || "Неизвестная ошибка"
|
||||
}. Пожалуйста, проверьте консоль.`
|
||||
);
|
||||
// В случае критической ошибки инициализации, завершаем все загрузки
|
||||
setIsMapLoading(false);
|
||||
setIsDataLoading(false);
|
||||
}
|
||||
@ -1607,7 +1742,6 @@ export const MapPage: React.FC = () => {
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
></div>
|
||||
{/* --- ИЗМЕНЕНИЕ: Обновленный лоадер --- */}
|
||||
{showLoader && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-500 bg-opacity-50 z-[1001]">
|
||||
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mb-3"></div>
|
||||
@ -1629,7 +1763,6 @@ export const MapPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* --- ИЗМЕНЕНИЕ: Условие для отображения контента --- */}
|
||||
{showContent && (
|
||||
<MapControls
|
||||
mapService={mapServiceInstance}
|
||||
|
Reference in New Issue
Block a user