feat: Add scale on group click, add cache for map entities, fix map preview loading

This commit is contained in:
2025-07-15 05:29:27 +03:00
parent 97f95fc394
commit 89d7fc2748
7 changed files with 547 additions and 299 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { useTransform } from "./TransformContext"; import { useTransform } from "./TransformContext";
import { coordinatesToLocal, localToCoordinates } from "./utils"; import { coordinatesToLocal, localToCoordinates } from "./utils";
import { SCALE_FACTOR } from "./Constants"; import { SCALE_FACTOR } from "./Constants";
import { toast } from "react-toastify";
export function RightSidebar() { export function RightSidebar() {
const { const {
@ -360,8 +361,14 @@ export function RightSidebar() {
variant="contained" variant="contained"
color="secondary" color="secondary"
sx={{ mt: 2 }} sx={{ mt: 2 }}
onClick={() => { onClick={async () => {
saveChanges(); try {
await saveChanges();
toast.success("Изменения сохранены");
} catch (error) {
console.error(error);
toast.error("Ошибка при сохранении изменений");
}
}} }}
> >
Сохранить изменения Сохранить изменения

View File

@ -26,6 +26,7 @@ import { Sight } from "./Sight";
import { SightData } from "./types"; import { SightData } from "./types";
import { Station } from "./Station"; import { Station } from "./Station";
import { UP_SCALE } from "./Constants"; import { UP_SCALE } from "./Constants";
import CircularProgress from "@mui/material/CircularProgress";
extend({ extend({
Container, Container,
@ -36,13 +37,27 @@ extend({
Text, Text,
}); });
const Loading = () => {
const { isRouteLoading, isStationLoading, isSightLoading } = useMapData();
if (isRouteLoading || isStationLoading || isSightLoading) {
return (
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
<CircularProgress />
</div>
);
}
return null;
};
export const RoutePreview = () => { export const RoutePreview = () => {
const { routeData, stationData, sightData } = useMapData();
return ( return (
<MapDataProvider> <MapDataProvider>
<TransformProvider> <TransformProvider>
<Stack direction="row" height="100vh" width="100vw" overflow="hidden"> <Stack direction="row" height="100vh" width="100vw" overflow="hidden">
<LanguageSwitcher /> {routeData && stationData && sightData ? <LanguageSwitcher /> : null}
<Loading />
<LeftSidebar /> <LeftSidebar />
<Stack direction="row" flex={1} position="relative" height="100%"> <Stack direction="row" flex={1} position="relative" height="100%">
<RouteMap /> <RouteMap />
@ -145,8 +160,7 @@ export const RouteMap = observer(() => {
]); ]);
if (!routeData || !stationData || !sightData) { if (!routeData || !stationData || !sightData) {
console.error("routeData, stationData or sightData is null"); return null;
return <div>Loading...</div>;
} }
return ( return (

View File

@ -81,6 +81,7 @@ export const UploadMediaDialog = observer(
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>( const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
[] []
); );
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
useEffect(() => { useEffect(() => {
if (initialFile) { if (initialFile) {
@ -207,6 +208,7 @@ export const UploadMediaDialog = observer(
useEffect(() => { useEffect(() => {
if (mediaFile) { if (mediaFile) {
setMediaUrl(URL.createObjectURL(mediaFile as Blob)); setMediaUrl(URL.createObjectURL(mediaFile as Blob));
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
} }
}, [mediaFile]); }, [mediaFile]);
@ -326,8 +328,22 @@ export const UploadMediaDialog = observer(
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
height: "100%", height: "100%",
position: "relative",
}} }}
> >
{!isPreviewLoaded && mediaUrl && (
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1,
}}
>
<CircularProgress />
</Box>
)}
{mediaType == 2 && mediaUrl && ( {mediaType == 2 && mediaUrl && (
<video <video
src={mediaUrl} src={mediaUrl}
@ -336,10 +352,16 @@ export const UploadMediaDialog = observer(
loop loop
controls controls
style={{ maxWidth: "100%", maxHeight: "100%" }} style={{ maxWidth: "100%", maxHeight: "100%" }}
onLoadedData={() => setIsPreviewLoaded(true)}
onError={() => setIsPreviewLoaded(true)}
/> />
)} )}
{mediaType === 6 && mediaUrl && ( {mediaType === 6 && mediaUrl && (
<ModelViewer3D fileUrl={mediaUrl} height="100%" /> <ModelViewer3D
fileUrl={mediaUrl}
height="100%"
onLoad={() => setIsPreviewLoaded(true)}
/>
)} )}
{mediaType !== 6 && mediaType !== 2 && mediaUrl && ( {mediaType !== 6 && mediaType !== 2 && mediaUrl && (
<img <img
@ -349,6 +371,8 @@ export const UploadMediaDialog = observer(
height: "100%", height: "100%",
objectFit: "contain", objectFit: "contain",
}} }}
onLoad={() => setIsPreviewLoaded(true)}
onError={() => setIsPreviewLoaded(true)}
/> />
)} )}
</Paper> </Paper>
@ -370,9 +394,17 @@ export const UploadMediaDialog = observer(
) )
} }
onClick={handleSave} onClick={handleSave}
disabled={isLoading || (!mediaName && !mediaFilename)} disabled={
isLoading ||
(!mediaName && !mediaFilename) ||
!isPreviewLoaded
}
> >
{isLoading ? "Сохранение..." : "Сохранить"} {isLoading
? "Сохранение..."
: !isPreviewLoaded
? "Загрузка превью..."
: "Сохранить"}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@ -97,15 +97,19 @@ class SightsStore {
city: number, city: number,
coordinates: { latitude: number; longitude: number } coordinates: { latitude: number; longitude: number }
) => { ) => {
const id = ( const response = await authInstance.post("/sight", {
await authInstance.post("/sight", { name: this.createSight[languageStore.language].name,
name: this.createSight[languageStore.language].name, address: this.createSight[languageStore.language].address,
address: this.createSight[languageStore.language].address, city_id: city,
city_id: city, latitude: coordinates.latitude,
latitude: coordinates.latitude, longitude: coordinates.longitude,
longitude: coordinates.longitude, });
})
).data.id; runInAction(() => {
this.sights.push(response.data);
});
const id = response.data.id;
const anotherLanguages = ["ru", "en", "zh"].filter( const anotherLanguages = ["ru", "en", "zh"].filter(
(language) => language !== languageStore.language (language) => language !== languageStore.language

View File

@ -51,7 +51,7 @@ export const LanguageSwitcher = observer(() => {
key={lang} key={lang}
onClick={() => handleLanguageChange(lang)} onClick={() => handleLanguageChange(lang)}
variant={"contained"} // Highlight the active language variant={"contained"} // Highlight the active language
color={language === lang ? "primary" : "secondary"} color={language === lang ? "primary" : "inherit"}
sx={{ minWidth: "60px" }} // Give buttons a consistent width sx={{ minWidth: "60px" }} // Give buttons a consistent width
> >
{getLanguageLabel(lang)} {getLanguageLabel(lang)}

View File

@ -1,3 +1,4 @@
import React from "react";
import { Stage, useGLTF } from "@react-three/drei"; import { Stage, useGLTF } from "@react-three/drei";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei"; import { OrbitControls } from "@react-three/drei";
@ -5,12 +6,21 @@ import { OrbitControls } from "@react-three/drei";
export const ModelViewer3D = ({ export const ModelViewer3D = ({
fileUrl, fileUrl,
height = "100%", height = "100%",
onLoad,
}: { }: {
fileUrl: string; fileUrl: string;
height: string; height: string;
onLoad?: () => void;
}) => { }) => {
const { scene } = useGLTF(fileUrl); const { scene } = useGLTF(fileUrl);
// Вызываем onLoad когда модель загружена
React.useEffect(() => {
if (onLoad) {
onLoad();
}
}, [scene, onLoad]);
return ( return (
<Canvas style={{ width: "100%", height: height }}> <Canvas style={{ width: "100%", height: height }}>
<ambientLight /> <ambientLight />