feat: Fixed path for routes

This commit is contained in:
2025-06-09 14:07:51 +03:00
parent 64c15b2622
commit 2ca1f2cba4
13 changed files with 706 additions and 32 deletions

373
public/Emblem.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 176 KiB

BIN
public/GET.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
public/SightIcon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

BIN
public/favicon_ship.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -38,6 +38,7 @@ import {
StationEditPage,
RouteCreatePage,
RoutePreview,
RouteEditPage,
} from "@pages";
import { authStore, createSightStore, editSightStore } from "@shared";
import { Layout } from "@widgets";
@ -98,6 +99,7 @@ const router = createBrowserRouter([
</PublicRoute>
),
},
{ path: "route-preview/:id", element: <RoutePreview /> },
{
path: "/",
element: (
@ -141,7 +143,7 @@ const router = createBrowserRouter([
// Route
{ path: "route", element: <RouteListPage /> },
{ path: "route/create", element: <RouteCreatePage /> },
{ path: "route-preview/:id", element: <RoutePreview /> },
{ path: "route/:id/edit", element: <RouteEditPage /> },
// User
{ path: "user", element: <UserListPage /> },

View File

@ -35,6 +35,7 @@ import {
Pencil,
Save,
Plus,
Loader2,
} from "lucide-react";
import { toast } from "react-toastify";
import { singleClick, doubleClick } from "ol/events/condition";
@ -1061,7 +1062,7 @@ const MapControls: React.FC<MapControlsProps> = ({
},
{
mode: "drawing-sight",
title: "Место",
title: "Достопримечательность",
longTitle: "Добавить достопримечательность",
icon: <Landmark size={16} className="mr-1 sm:mr-2" />,
action: () => mapService.startDrawingSight(),
@ -1109,6 +1110,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
}) => {
const [activeSection, setActiveSection] = useState<string | null>("layers");
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const toggleSection = (id: string) =>
setActiveSection(activeSection === id ? null : id);
@ -1124,7 +1126,9 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
(id: string | number | undefined, recourse: string) => {
if (
mapService &&
window.confirm("Вы действительно хотите удалить этот объект?")
window.confirm(
"Вы действительно хотите удалить этот объект? Утерянные данные не могут быть восстановлены."
)
)
mapService.deleteFeature(id, recourse);
},
@ -1144,8 +1148,16 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
[navigate]
);
const handleSave = useCallback(() => {
mapStore.handleSave(mapService?.getAllFeaturesAsGeoJSON() || "");
const handleSave = useCallback(async () => {
const geoJSON = mapService?.getAllFeaturesAsGeoJSON();
if (geoJSON) {
setIsLoading(true);
await mapStore.handleSave(geoJSON);
toast.success("Данные успешно сохранены");
} else {
toast.error("Ошибка при сохранении данных");
}
setIsLoading(false);
}, [mapService]);
const stations = mapFeatures.filter(
@ -1470,10 +1482,15 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
<button
onClick={handleSave}
className="m-3 w-[90%] flex items-center justify-center px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
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" />
) : (
"Сохранить изменения"
)}
</button>
</div>
);

View File

@ -34,13 +34,27 @@ class MapStore {
getRoutes = async () => {
const routes = await languageInstance("ru").get("/route");
const mappedRoutes = routes.data.map((route: any) => ({
id: route.id,
route_number: route.route_number,
path: route.path,
}));
const routedIds = routes.data.map((route: any) => route.id);
this.routes = mappedRoutes;
const mappedRoutes: ApiRoute[] = [];
for (const routeId of routedIds) {
const responseSoloRoute = await languageInstance("ru").get(
`/route/${routeId}`
);
const route = responseSoloRoute.data;
const mappedRoute = {
id: route.id,
route_number: route.route_number,
path: route.path,
};
mappedRoutes.push(mappedRoute);
}
this.routes = mappedRoutes.sort((a, b) =>
a.route_number.localeCompare(b.route_number)
);
};
getStations = async () => {

View File

@ -43,7 +43,7 @@ export const MediaListPage = observer(() => {
{
field: "actions",
headerName: "Действия",
flex: 1,
width: 200,
align: "center",
headerAlign: "center",
@ -75,8 +75,6 @@ export const MediaListPage = observer(() => {
return (
<>
<LanguageSwitcher />
<div className="w-full">
<div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Медиа</h1>

View File

@ -0,0 +1,225 @@
import {
Button,
Paper,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Typography,
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { carrierStore } from "../../../shared/store/CarrierStore";
import { articlesStore } from "../../../shared/store/ArticlesStore";
import { routeStore } from "../../../shared/store/RouteStore";
import { toast } from "react-toastify";
export const RouteEditPage = observer(() => {
const navigate = useNavigate();
const { id } = useParams();
const { editRouteData } = routeStore;
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
const response = await routeStore.getRoute(Number(id));
routeStore.setEditRouteData(response);
carrierStore.getCarriers();
articlesStore.getArticleList();
};
fetchData();
}, [id]);
const handleSave = async () => {
setIsLoading(true);
await routeStore.editRoute(Number(id));
toast.success("Маршрут успешно сохранен");
setIsLoading(false);
};
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>
<Typography variant="h5" fontWeight={700}>
Редактировать маршрут
</Typography>
<Box className="flex flex-col gap-6 w-full">
<FormControl fullWidth required>
<InputLabel>Выберите перевозчика</InputLabel>
<Select
value={editRouteData.carrier_id}
label="Выберите перевозчика"
onChange={(e) =>
routeStore.setEditRouteData({
carrier_id: Number(e.target.value),
carrier:
carrierStore.carriers.data.find(
(c) => c.id === Number(e.target.value)
)?.full_name || "",
})
}
disabled={carrierStore.carriers.data.length === 0}
>
<MenuItem value="">Не выбрано</MenuItem>
{carrierStore.carriers.data.map(
(c: (typeof carrierStore.carriers.data)[number]) => (
<MenuItem key={c.id} value={c.id}>
{c.full_name}
</MenuItem>
)
)}
</Select>
</FormControl>
<TextField
className="w-full"
label="Номер маршрута"
required
value={editRouteData.route_number || ""}
onChange={(e) =>
routeStore.setEditRouteData({
route_number: e.target.value,
})
}
/>
<TextField
className="w-full"
label="Координаты маршрута"
multiline
minRows={3}
value={editRouteData.path.map((p) => p.join(" ")).join("\n") || ""}
onChange={(e) =>
routeStore.setEditRouteData({
path: e.target.value
.split("\n")
.map((line) => line.split(" ").map(Number)),
})
}
/>
<TextField
className="w-full"
label="Номер маршрута в Говорящем Городе"
required
value={editRouteData.route_sys_number || ""}
onChange={(e) =>
routeStore.setEditRouteData({
route_sys_number: e.target.value,
})
}
/>
<FormControl fullWidth required>
<InputLabel>Обращение губернатора</InputLabel>
<Select
value={editRouteData.governor_appeal || ""}
label="Обращение губернатора"
onChange={(e) =>
routeStore.setEditRouteData({
governor_appeal: Number(e.target.value),
})
}
disabled={articlesStore.articleList.ru.data.length === 0}
>
<MenuItem value="">Не выбрано</MenuItem>
{articlesStore.articleList.ru.data.map(
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
<MenuItem key={a.id} value={a.id}>
{a.heading}
</MenuItem>
)
)}
</Select>
</FormControl>
<FormControl fullWidth required>
<InputLabel>Прямой/обратный маршрут</InputLabel>
<Select
value={editRouteData.route_direction ? "forward" : "backward"}
label="Прямой/обратный маршрут"
onChange={(e) =>
routeStore.setEditRouteData({
route_direction: e.target.value === "forward",
})
}
>
<MenuItem value="forward">Прямой</MenuItem>
<MenuItem value="backward">Обратный</MenuItem>
</Select>
</FormControl>
<TextField
className="w-full"
label="Масштаб (мин)"
value={editRouteData.scale_min || ""}
onChange={(e) =>
routeStore.setEditRouteData({
scale_min: Number(e.target.value),
})
}
/>
<TextField
className="w-full"
label="Масштаб (макс)"
value={editRouteData.scale_max || ""}
onChange={(e) =>
routeStore.setEditRouteData({
scale_max: Number(e.target.value),
})
}
/>
<TextField
className="w-full"
label="Поворот"
value={editRouteData.rotate || ""}
onChange={(e) =>
routeStore.setEditRouteData({
rotate: Number(e.target.value),
})
}
/>
<TextField
className="w-full"
label="Центр. широта"
value={editRouteData.center_latitude || ""}
onChange={(e) =>
routeStore.setEditRouteData({
center_latitude: Number(e.target.value),
})
}
/>
<TextField
className="w-full"
label="Центр. долгота"
value={editRouteData.center_longitude || ""}
onChange={(e) =>
routeStore.setEditRouteData({
center_longitude: Number(e.target.value),
})
}
/>
</Box>
<div className="flex w-full justify-end">
<Button
variant="contained"
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleSave}
disabled={isLoading}
>
Сохранить
</Button>
</div>
</Paper>
);
});

View File

@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { languageStore, routeStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Map, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
@ -49,15 +49,21 @@ export const RouteListPage = observer(() => {
{
field: "actions",
headerName: "Действия",
width: 140,
width: 250,
align: "center",
headerAlign: "center",
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button onClick={() => navigate(`/route/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
<Map size={20} className="text-purple-500" />
</button>
{/* <button onClick={() => navigate(`/route/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
</button> */}
<button
onClick={() => {
setIsDeleteModalOpen(true);

View File

@ -1,3 +1,4 @@
export * from "./RouteListPage";
export * from "./RouteCreatePage";
export { RoutePreview } from "./route-preview";
export * from "./RouteEditPage";

View File

@ -38,18 +38,9 @@ export const RoutePreview = () => {
return (
<MapDataProvider>
<TransformProvider>
<Stack direction="row" height="90vh" width="90vw" overflow="hidden">
<div
style={{
position: "absolute",
top: 0,
left: "50%",
transform: "translateX(-50%)",
zIndex: 1000,
}}
>
<LanguageSwitcher />
</div>
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
<LanguageSwitcher />
<LeftSidebar />
<Stack direction="row" flex={1} position="relative" height="100%">
<Widgets />

View File

@ -26,6 +26,7 @@ class RouteStore {
data: [],
loaded: false,
};
route: Record<string, Route> = {};
constructor() {
@ -64,6 +65,52 @@ class RouteStore {
};
});
};
getRoute = async (id: number) => {
if (this.route[id]) return this.route[id];
const response = await authInstance.get(`/route/${id}`);
runInAction(() => {
this.route[id] = response.data;
});
return response.data;
};
editRouteData = {
carrier: "",
carrier_id: 0,
center_latitude: 0,
center_longitude: 0,
governor_appeal: 0,
id: 0,
path: [] as number[][],
rotate: 0,
route_direction: false,
route_number: "",
route_sys_number: "",
scale_max: 0,
scale_min: 0,
video_preview: "",
};
setEditRouteData = (data: any) => {
this.editRouteData = { ...this.editRouteData, ...data };
};
editRoute = async (id: number) => {
const response = await authInstance.patch(
`/route/${id}`,
this.editRouteData
);
runInAction(() => {
this.route[id] = response.data;
this.routes.data = this.routes.data.map((route) =>
route.id === id ? response.data : route
);
});
};
}
export const routeStore = new RouteStore();