fix: Fix problems and bugs

This commit is contained in:
2025-07-28 08:18:21 +03:00
parent 470a58a3fa
commit 4f038551a2
7 changed files with 381 additions and 148 deletions

View File

@ -75,6 +75,19 @@ import { makeAutoObservable } from "mobx";
import { stationsStore, routeStore, sightsStore } from "@shared"; import { stationsStore, routeStore, sightsStore } from "@shared";
// Функция для сброса кешей карты
export const clearMapCaches = () => {
// Сброс кешей маршрутов
mapStore.routes = [];
mapStore.stations = [];
mapStore.sights = [];
// Сброс кешей MapService если он доступен
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
(window as any).mapServiceInstance.clearCaches();
}
};
interface ApiRoute { interface ApiRoute {
id: number; id: number;
route_number: string; route_number: string;
@ -328,6 +341,11 @@ class MapStore {
const mapStore = new MapStore(); const mapStore = new MapStore();
// Делаем mapStore доступным глобально для сброса кешей
if (typeof window !== "undefined") {
(window as any).mapStore = mapStore;
}
// --- CONFIGURATION --- // --- CONFIGURATION ---
export const mapConfig = { export const mapConfig = {
center: [30.311, 59.94] as [number, number], center: [30.311, 59.94] as [number, number],
@ -453,7 +471,7 @@ class MapService {
public routeLayer: VectorLayer<VectorSource<Feature<LineString>>>; // Public for deselect public routeLayer: VectorLayer<VectorSource<Feature<LineString>>>; // Public for deselect
private clusterSource: Cluster; private clusterSource: Cluster;
private clusterStyleCache: { [key: number]: Style }; private clusterStyleCache: { [key: number]: Style };
private unclusteredRouteIds: Set<string | number> = new Set();
private tooltipElement: HTMLElement; private tooltipElement: HTMLElement;
private tooltipOverlay: Overlay | null; private tooltipOverlay: Overlay | null;
private mode: string | null; private mode: string | null;
@ -488,8 +506,7 @@ class MapService {
private sightIconStyle: Style; private sightIconStyle: Style;
private selectedSightIconStyle: Style; private selectedSightIconStyle: Style;
private drawSightIconStyle: Style; private drawSightIconStyle: Style;
private routeIconStyle: Style;
private selectedRouteIconStyle: Style;
private universalHoverStylePoint: Style; private universalHoverStylePoint: Style;
private hoverSightIconStyle: Style; private hoverSightIconStyle: Style;
private universalHoverStyleLine: Style; private universalHoverStyleLine: Style;
@ -574,21 +591,6 @@ class MapService {
}), }),
}); });
this.routeIconStyle = new Style({
image: new CircleStyle({
radius: 8,
fill: new Fill({ color: "rgba(34, 197, 94, 0.8)" }), // Green
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
}),
});
this.selectedRouteIconStyle = new Style({
image: new CircleStyle({
radius: 10,
fill: new Fill({ color: "rgba(221, 107, 32, 0.9)" }), // Orange on select
stroke: new Stroke({ color: "#ffffff", width: 2 }),
}),
});
this.sightIconStyle = new Style({ this.sightIconStyle = new Style({
image: new RegularShape({ image: new RegularShape({
fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }), fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }),
@ -659,10 +661,7 @@ class MapService {
if (!feature) return this.defaultStyle; if (!feature) return this.defaultStyle;
const fId = feature.getId(); const fId = feature.getId();
if (fId === undefined || !this.unclusteredRouteIds.has(fId)) { // Все маршруты всегда отображаются, так как они не кластеризуются
return null;
}
const isSelected = const isSelected =
this.selectInteraction?.getFeatures().getArray().includes(feature) || this.selectInteraction?.getFeatures().getArray().includes(feature) ||
(fId !== undefined && this.selectedIds.has(fId)); (fId !== undefined && this.selectedIds.has(fId));
@ -705,8 +704,6 @@ class MapService {
const originalFeature = featuresInCluster[0]; const originalFeature = featuresInCluster[0];
const fId = originalFeature.getId(); const fId = originalFeature.getId();
const featureType = originalFeature.get("featureType"); const featureType = originalFeature.get("featureType");
const isProxy = originalFeature.get("isProxy");
if (isProxy) return new Style(); // Invisible empty style
const isSelected = fId !== undefined && this.selectedIds.has(fId); const isSelected = fId !== undefined && this.selectedIds.has(fId);
const isHovered = this.hoveredFeatureId === fId; const isHovered = this.hoveredFeatureId === fId;
@ -719,45 +716,20 @@ class MapService {
if (isSelected) { if (isSelected) {
if (featureType === "sight") return this.selectedSightIconStyle; if (featureType === "sight") return this.selectedSightIconStyle;
if (featureType === "route") return this.selectedRouteIconStyle;
return this.selectedBusIconStyle; return this.selectedBusIconStyle;
} }
if (featureType === "sight") return this.sightIconStyle; if (featureType === "sight") return this.sightIconStyle;
if (featureType === "route") return this.routeIconStyle;
return this.busIconStyle; return this.busIconStyle;
} }
}, },
}); });
this.clusterSource.on("change", () => { this.clusterSource.on("change", () => {
const newUnclusteredRouteIds = new Set<string | number>(); // Поскольку маршруты больше не добавляются как точки,
this.clusterSource // нам не нужно отслеживать unclusteredRouteIds
.getFeatures() // Все маршруты всегда отображаются как линии
.forEach((clusterFeature: Feature<any>) => {
const originalFeatures = clusterFeature.get(
"features"
) as Feature<Point>[];
if (originalFeatures && originalFeatures.length === 1) {
const originalFeature = originalFeatures[0];
if (originalFeature.get("featureType") === "route") {
const featureId = originalFeature.getId();
if (featureId !== undefined) {
newUnclusteredRouteIds.add(featureId);
}
}
}
});
if (
newUnclusteredRouteIds.size !== this.unclusteredRouteIds.size ||
![...newUnclusteredRouteIds].every((id) =>
this.unclusteredRouteIds.has(id)
)
) {
this.unclusteredRouteIds = newUnclusteredRouteIds;
this.routeLayer.changed(); this.routeLayer.changed();
}
}); });
this.boundHandlePointerMove = this.handlePointerMove.bind(this); this.boundHandlePointerMove = this.handlePointerMove.bind(this);
@ -1209,23 +1181,7 @@ class MapService {
lineFeature.set("featureType", "route"); lineFeature.set("featureType", "route");
lineFeatures.push(lineFeature); lineFeatures.push(lineFeature);
if (route.center_longitude != null && route.center_latitude != null) { // Не создаем прокси-точки для маршрутов - они должны оставаться только линиями
const centerPoint = new Point(
transform(
[route.center_longitude, route.center_latitude],
"EPSG:4326",
projection
)
);
const proxyPointFeature = new Feature({
geometry: centerPoint,
name: route.route_number,
isProxy: true,
});
proxyPointFeature.setId(routeId);
proxyPointFeature.set("featureType", "route");
pointFeatures.push(proxyPointFeature);
}
}); });
this.pointSource.addFeatures(pointFeatures); this.pointSource.addFeatures(pointFeatures);
@ -1926,6 +1882,32 @@ class MapService {
return this.map; return this.map;
} }
// Метод для сброса кешей карты
public clearCaches() {
this.clusterStyleCache = {};
this.history = [];
this.historyIndex = -1;
this.beforeActionState = null;
this.hoveredFeatureId = null;
this.selectedIds.clear();
// Очищаем источники данных
if (this.pointSource) {
this.pointSource.clear();
}
if (this.lineSource) {
this.lineSource.clear();
}
// Обновляем слои
if (this.clusterLayer) {
this.clusterLayer.changed();
}
if (this.routeLayer) {
this.routeLayer.changed();
}
}
public saveCurrentPosition(): void { public saveCurrentPosition(): void {
if (!this.map) return; if (!this.map) return;
const center = this.map.getView().getCenter(); const center = this.map.getView().getCenter();
@ -1941,20 +1923,6 @@ class MapService {
const featureId = feature.getId(); const featureId = feature.getId();
if (!featureType || featureId === undefined || !this.map) return; if (!featureType || featureId === undefined || !this.map) return;
if (
featureType === "route" &&
feature.getGeometry()?.getType() === "LineString"
) {
const proxyPoint = this.pointSource.getFeatureById(
featureId
) as Feature<Point>;
if (proxyPoint) {
const lineGeom = feature.getGeometry() as LineString;
const newCenter = getCenter(lineGeom.getExtent());
proxyPoint.getGeometry()?.setCoordinates(newCenter);
}
}
if (typeof featureId === "number" || !String(featureId).includes("-")) { if (typeof featureId === "number" || !String(featureId).includes("-")) {
console.warn( console.warn(
"Skipping save for feature with non-standard ID:", "Skipping save for feature with non-standard ID:",
@ -2023,22 +1991,7 @@ class MapService {
); );
feature.setGeometry(lineGeom); feature.setGeometry(lineGeom);
// Create and add proxy point // Не создаем прокси-точку для маршрута - только линия
const centerPointGeom = new Point(
transform(
[routeData.center_longitude, routeData.center_latitude],
"EPSG:4326",
projection
)
);
const proxyPointFeature = new Feature({
geometry: centerPointGeom,
name: displayName,
isProxy: true,
});
proxyPointFeature.setId(newFeatureId);
proxyPointFeature.set("featureType", "route");
this.pointSource.addFeature(proxyPointFeature);
} else { } else {
// For points: update existing // For points: update existing
feature.setId(newFeatureId); feature.setId(newFeatureId);
@ -2603,6 +2556,12 @@ export const MapPage: React.FC = () => {
setSelectedIds setSelectedIds
); );
setMapServiceInstance(service); setMapServiceInstance(service);
// Делаем mapServiceInstance доступным глобально для сброса кешей
if (typeof window !== "undefined") {
(window as any).mapServiceInstance = service;
}
loadInitialData(service); loadInitialData(service);
} catch (e: any) { } catch (e: any) {
setError( setError(
@ -2615,6 +2574,11 @@ export const MapPage: React.FC = () => {
return () => { return () => {
service?.destroy(); service?.destroy();
setMapServiceInstance(null); setMapServiceInstance(null);
// Удаляем глобальную ссылку
if (typeof window !== "undefined") {
delete (window as any).mapServiceInstance;
}
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View File

@ -225,7 +225,7 @@ export const LinkedItemsContents = <
if (type === "edit") { if (type === "edit") {
setError(null); setError(null);
authInstance authInstance
.get(`/${childResource}/`) .get(`/${childResource}`)
.then((response) => { .then((response) => {
setAllItems(response?.data || []); setAllItems(response?.data || []);
}) })

View File

@ -1,4 +1,4 @@
import { Button, Paper, TextField } from "@mui/material"; import { Button, TextField } from "@mui/material";
import { snapshotStore } from "@shared"; import { snapshotStore } from "@shared";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react"; import { ArrowLeft, Loader2, Save } from "lucide-react";
@ -20,7 +20,8 @@ export const SnapshotCreatePage = observer(() => {
}, [id]); }, [id]);
return ( return (
<Paper className="w-full h-full p-3 flex flex-col gap-10"> <div className="w-full h-[400px] flex justify-center items-center">
<div className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<button <button
className="flex items-center gap-2" className="flex items-center gap-2"
@ -51,11 +52,15 @@ export const SnapshotCreatePage = observer(() => {
await createSnapshot(name); await createSnapshot(name);
setIsLoading(false); setIsLoading(false);
toast.success("Снапшот успешно создан"); toast.success("Снапшот успешно создан");
navigate(-1);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error("Ошибка при создании снапшота");
} finally {
setIsLoading(false);
} }
}} }}
disabled={isLoading} disabled={isLoading || !name.trim()}
> >
{isLoading ? ( {isLoading ? (
<Loader2 size={20} className="animate-spin" /> <Loader2 size={20} className="animate-spin" />
@ -64,6 +69,7 @@ export const SnapshotCreatePage = observer(() => {
)} )}
</Button> </Button>
</div> </div>
</Paper> </div>
</div>
); );
}); });

View File

@ -1,3 +1,2 @@
export * from "./SnapshotListPage"; export * from "./SnapshotListPage";
export * from "./SnapshotCreatePage"; export * from "./SnapshotCreatePage";

View File

@ -176,7 +176,7 @@ export const LinkedSightsContents = <
if (type === "edit") { if (type === "edit") {
setError(null); setError(null);
authInstance authInstance
.get(`/${childResource}/`) .get(`/${childResource}`)
.then((response) => { .then((response) => {
setAllItems(response?.data || []); setAllItems(response?.data || []);
}) })

View File

@ -1,6 +1,24 @@
import { authInstance } from "@shared"; import { authInstance } from "@shared";
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
// Импорт функции сброса кешей карты
// import { clearMapCaches } from "../../pages/MapPage";
import {
articlesStore,
cityStore,
countryStore,
carrierStore,
stationsStore,
sightsStore,
routeStore,
vehicleStore,
userStore,
mediaStore,
createSightStore,
editSightStore,
devicesStore,
authStore,
} from "@shared";
type Snapshot = { type Snapshot = {
ID: string; ID: string;
@ -17,6 +35,248 @@ class SnapshotStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
// Функция для сброса всех кешей в приложении
private clearAllCaches = () => {
// Сброс кешей статей
articlesStore.articleList = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
articlesStore.articlePreview = {};
articlesStore.articleData = null;
articlesStore.articleMedia = null;
// Сброс кешей городов
cityStore.cities = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
cityStore.ruCities = { data: [], loaded: false };
cityStore.city = {};
// Сброс кешей стран
countryStore.countries = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
// Сброс кешей перевозчиков
carrierStore.carriers = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
// Сброс кешей станций
stationsStore.stationLists = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
stationsStore.stationPreview = {};
// Сброс кешей достопримечательностей
sightsStore.sights = [];
sightsStore.sight = null;
// Сброс кешей маршрутов
routeStore.routes = { data: [], loaded: false };
// Сброс кешей транспорта
vehicleStore.vehicles = { data: [], loaded: false };
// Сброс кешей пользователей
userStore.users = { data: [], loaded: false };
// Сброс кешей медиа
mediaStore.media = [];
mediaStore.oneMedia = null;
// Сброс кешей создания и редактирования достопримечательностей
createSightStore.sight = JSON.parse(
JSON.stringify({
city_id: 0,
city: "",
latitude: 0,
longitude: 0,
thumbnail: null,
watermark_lu: null,
watermark_rd: null,
left_article: 0,
preview_media: null,
video_preview: null,
ru: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
en: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
zh: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
})
);
createSightStore.uploadMediaOpen = false;
createSightStore.fileToUpload = null;
createSightStore.needLeaveAgree = false;
editSightStore.sight = {
common: {
id: 0,
city_id: 0,
city: "",
latitude: 0,
longitude: 0,
thumbnail: null,
watermark_lu: null,
watermark_rd: null,
left_article: 0,
preview_media: null,
video_preview: null,
},
ru: {
id: 0,
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
en: {
id: 0,
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
zh: {
id: 0,
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
};
editSightStore.hasLoadedCommon = false;
editSightStore.uploadMediaOpen = false;
editSightStore.fileToUpload = null;
editSightStore.needLeaveAgree = false;
// Сброс кешей устройств
devicesStore.devices = [];
devicesStore.uuid = null;
devicesStore.sendSnapshotModalOpen = false;
// Сброс кешей авторизации (кроме токена)
authStore.payload = null;
authStore.error = null;
authStore.isLoading = false;
// Сброс кешей карты (если они загружены)
try {
// Сбрасываем кеши mapStore если он доступен
if (typeof window !== "undefined" && (window as any).mapStore) {
(window as any).mapStore.routes = [];
(window as any).mapStore.stations = [];
(window as any).mapStore.sights = [];
}
// Сбрасываем кеши MapService если он доступен
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
(window as any).mapServiceInstance.clearCaches();
}
} catch (error) {
console.warn("Не удалось сбросить кеши карты:", error);
}
// Сброс localStorage кешей (кроме токена авторизации)
const token = localStorage.getItem("token");
const rememberedEmail = localStorage.getItem("rememberedEmail");
const rememberedPassword = localStorage.getItem("rememberedPassword");
localStorage.clear();
sessionStorage.clear();
// Восстанавливаем важные данные
if (token) localStorage.setItem("token", token);
if (rememberedEmail)
localStorage.setItem("rememberedEmail", rememberedEmail);
if (rememberedPassword)
localStorage.setItem("rememberedPassword", rememberedPassword);
// Сброс кешей карты (если они есть)
const mapPositionKey = "mapPosition";
const activeSectionKey = "mapActiveSection";
if (localStorage.getItem(mapPositionKey)) {
localStorage.removeItem(mapPositionKey);
}
if (localStorage.getItem(activeSectionKey)) {
localStorage.removeItem(activeSectionKey);
}
// Попытка очистить кеш браузера (если поддерживается)
if ("caches" in window) {
try {
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
return caches.delete(cacheName);
})
);
})
.then(() => {
console.log("Кеш браузера очищен");
})
.catch((error) => {
console.warn("Не удалось очистить кеш браузера:", error);
});
} catch (error) {
console.warn("Кеш браузера не поддерживается:", error);
}
}
// Попытка очистить IndexedDB (если поддерживается)
if ("indexedDB" in window) {
try {
indexedDB
.databases()
.then((databases) => {
return Promise.all(
databases.map((db) => {
if (db.name) {
return indexedDB.deleteDatabase(db.name);
}
return Promise.resolve();
})
);
})
.then(() => {
console.log("IndexedDB очищен");
})
.catch((error) => {
console.warn("Не удалось очистить IndexedDB:", error);
});
} catch (error) {
console.warn("IndexedDB не поддерживается:", error);
}
}
console.log("Все кеши приложения сброшены");
};
getSnapshots = async () => { getSnapshots = async () => {
const response = await authInstance.get(`/snapshots`); const response = await authInstance.get(`/snapshots`);
@ -42,6 +302,10 @@ class SnapshotStore {
}; };
restoreSnapshot = async (id: string) => { restoreSnapshot = async (id: string) => {
// Сначала сбрасываем все кеши
this.clearAllCaches();
// Затем восстанавливаем снапшот
await authInstance.post(`/snapshots/${id}/restore`); await authInstance.post(`/snapshots/${id}/restore`);
}; };

File diff suppressed because one or more lines are too long