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";
// Функция для сброса кешей карты
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 {
id: number;
route_number: string;
@ -328,6 +341,11 @@ class MapStore {
const mapStore = new MapStore();
// Делаем mapStore доступным глобально для сброса кешей
if (typeof window !== "undefined") {
(window as any).mapStore = mapStore;
}
// --- CONFIGURATION ---
export const mapConfig = {
center: [30.311, 59.94] as [number, number],
@ -453,7 +471,7 @@ class MapService {
public routeLayer: VectorLayer<VectorSource<Feature<LineString>>>; // Public for deselect
private clusterSource: Cluster;
private clusterStyleCache: { [key: number]: Style };
private unclusteredRouteIds: Set<string | number> = new Set();
private tooltipElement: HTMLElement;
private tooltipOverlay: Overlay | null;
private mode: string | null;
@ -488,8 +506,7 @@ class MapService {
private sightIconStyle: Style;
private selectedSightIconStyle: Style;
private drawSightIconStyle: Style;
private routeIconStyle: Style;
private selectedRouteIconStyle: Style;
private universalHoverStylePoint: Style;
private hoverSightIconStyle: 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({
image: new RegularShape({
fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }),
@ -659,10 +661,7 @@ class MapService {
if (!feature) return this.defaultStyle;
const fId = feature.getId();
if (fId === undefined || !this.unclusteredRouteIds.has(fId)) {
return null;
}
// Все маршруты всегда отображаются, так как они не кластеризуются
const isSelected =
this.selectInteraction?.getFeatures().getArray().includes(feature) ||
(fId !== undefined && this.selectedIds.has(fId));
@ -705,8 +704,6 @@ class MapService {
const originalFeature = featuresInCluster[0];
const fId = originalFeature.getId();
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 isHovered = this.hoveredFeatureId === fId;
@ -719,45 +716,20 @@ class MapService {
if (isSelected) {
if (featureType === "sight") return this.selectedSightIconStyle;
if (featureType === "route") return this.selectedRouteIconStyle;
return this.selectedBusIconStyle;
}
if (featureType === "sight") return this.sightIconStyle;
if (featureType === "route") return this.routeIconStyle;
return this.busIconStyle;
}
},
});
this.clusterSource.on("change", () => {
const newUnclusteredRouteIds = new Set<string | number>();
this.clusterSource
.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();
}
// Поскольку маршруты больше не добавляются как точки,
// нам не нужно отслеживать unclusteredRouteIds
// Все маршруты всегда отображаются как линии
this.routeLayer.changed();
});
this.boundHandlePointerMove = this.handlePointerMove.bind(this);
@ -1209,23 +1181,7 @@ class MapService {
lineFeature.set("featureType", "route");
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);
@ -1926,6 +1882,32 @@ class MapService {
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 {
if (!this.map) return;
const center = this.map.getView().getCenter();
@ -1941,20 +1923,6 @@ class MapService {
const featureId = feature.getId();
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("-")) {
console.warn(
"Skipping save for feature with non-standard ID:",
@ -2023,22 +1991,7 @@ class MapService {
);
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 {
// For points: update existing
feature.setId(newFeatureId);
@ -2603,6 +2556,12 @@ export const MapPage: React.FC = () => {
setSelectedIds
);
setMapServiceInstance(service);
// Делаем mapServiceInstance доступным глобально для сброса кешей
if (typeof window !== "undefined") {
(window as any).mapServiceInstance = service;
}
loadInitialData(service);
} catch (e: any) {
setError(
@ -2615,6 +2574,11 @@ export const MapPage: React.FC = () => {
return () => {
service?.destroy();
setMapServiceInstance(null);
// Удаляем глобальную ссылку
if (typeof window !== "undefined") {
delete (window as any).mapServiceInstance;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@ -225,7 +225,7 @@ export const LinkedItemsContents = <
if (type === "edit") {
setError(null);
authInstance
.get(`/${childResource}/`)
.get(`/${childResource}`)
.then((response) => {
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 { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
@ -20,50 +20,56 @@ export const SnapshotCreatePage = observer(() => {
}, [id]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex justify-between items-center">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<h1 className="text-2xl font-bold">Создание снапшота</h1>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
className="w-full"
label="Название"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
<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">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<h1 className="text-2xl font-bold">Создание снапшота</h1>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
className="w-full"
label="Название"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button
variant="contained"
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={async () => {
try {
setIsLoading(true);
await createSnapshot(name);
setIsLoading(false);
toast.success("Снапшот успешно создан");
} catch (error) {
console.error(error);
}
}}
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Сохранить"
)}
</Button>
<Button
variant="contained"
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={async () => {
try {
setIsLoading(true);
await createSnapshot(name);
setIsLoading(false);
toast.success("Снапшот успешно создан");
navigate(-1);
} catch (error) {
console.error(error);
toast.error("Ошибка при создании снапшота");
} finally {
setIsLoading(false);
}
}}
disabled={isLoading || !name.trim()}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Сохранить"
)}
</Button>
</div>
</div>
</Paper>
</div>
);
});

View File

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

View File

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

View File

@ -1,6 +1,24 @@
import { authInstance } from "@shared";
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 = {
ID: string;
@ -17,6 +35,248 @@ class SnapshotStore {
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 () => {
const response = await authInstance.get(`/snapshots`);
@ -42,6 +302,10 @@ class SnapshotStore {
};
restoreSnapshot = async (id: string) => {
// Сначала сбрасываем все кеши
this.clearAllCaches();
// Затем восстанавливаем снапшот
await authInstance.post(`/snapshots/${id}/restore`);
};

File diff suppressed because one or more lines are too long