From f4544c18884ecc03291efce4dca82ef599ac952f Mon Sep 17 00:00:00 2001 From: itoshi Date: Mon, 9 Jun 2025 09:17:56 +0300 Subject: [PATCH] feat: Route list page --- src/app/router/index.tsx | 13 +- src/entities/navigation/ui/index.tsx | 16 +- src/pages/Article/ArticleListPage/index.tsx | 8 +- src/pages/Carrier/CarrierCreatePage/index.tsx | 2 +- src/pages/Carrier/CarrierEditPage/index.tsx | 2 +- src/pages/Carrier/CarrierListPage/index.tsx | 2 +- src/pages/City/CityCreatePage/index.tsx | 2 +- src/pages/City/CityEditPage/index.tsx | 2 +- src/pages/Country/CountryCreatePage/index.tsx | 2 +- src/pages/Country/CountryEditPage/index.tsx | 2 +- src/pages/MapPage/index.tsx | 638 +++++++++++------- src/pages/MapPage/mapStore.ts | 129 ++++ src/pages/Media/MediaEditPage/index.tsx | 2 +- src/pages/Route/RouteCreatePage/index.tsx | 200 +++++- src/pages/Route/RouteListPage/index.tsx | 4 +- src/pages/Route/index.ts | 3 +- src/pages/Sight/SightListPage/index.tsx | 2 +- src/pages/Snapshot/SnapshotListPage/index.tsx | 13 +- src/pages/Station/StationEditPage/index.tsx | 179 +++++ src/pages/Station/StationListPage/index.tsx | 11 +- .../Station/StationPreviewPage/index.tsx | 77 +++ src/pages/Station/index.ts | 3 + src/pages/User/UserCreatePage/index.tsx | 2 +- src/pages/User/UserEditPage/index.tsx | 2 +- src/pages/User/UserListPage/index.tsx | 2 +- src/pages/Vehicle/VehicleCreatePage/index.tsx | 6 +- src/pages/Vehicle/VehicleEditPage/index.tsx | 2 +- src/pages/Vehicle/VehicleListPage/index.tsx | 20 +- src/shared/config/constants.tsx | 26 +- src/shared/store/ArticlesStore/index.tsx | 55 +- src/shared/store/CarrierStore/index.tsx | 27 +- src/shared/store/CityStore/index.ts | 64 +- src/shared/store/RouteStore/index.ts | 32 +- src/shared/store/StationsStore/index.ts | 342 +++++++++- src/shared/store/UserStore/index.ts | 26 +- src/shared/store/VehicleStore/index.ts | 19 +- tsconfig.tsbuildinfo | 2 +- 37 files changed, 1539 insertions(+), 400 deletions(-) create mode 100644 src/pages/MapPage/mapStore.ts create mode 100644 src/pages/Station/StationEditPage/index.tsx create mode 100644 src/pages/Station/StationPreviewPage/index.tsx diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index 089a9bb..b698aa2 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -33,6 +33,10 @@ import { UserEditPage, VehicleEditPage, CarrierEditPage, + StationCreatePage, + StationPreviewPage, + StationEditPage, + RouteCreatePage, } from "@pages"; import { authStore, createSightStore, editSightStore } from "@shared"; import { Layout } from "@widgets"; @@ -110,7 +114,7 @@ const router = createBrowserRouter([ // Sight { path: "sight", element: }, { path: "sight/create", element: }, - { path: "sight/:id", element: }, + { path: "sight/:id/edit", element: }, // Device { path: "devices", element: }, @@ -135,6 +139,7 @@ const router = createBrowserRouter([ { path: "city/:id/edit", element: }, // Route { path: "route", element: }, + { path: "route/create", element: }, // User { path: "user", element: }, @@ -151,7 +156,9 @@ const router = createBrowserRouter([ { path: "carrier/:id/edit", element: }, // Station { path: "station", element: }, - + { path: "station/create", element: }, + { path: "station/:id", element: }, + { path: "station/:id/edit", element: }, // Vehicle { path: "vehicle", element: }, { path: "vehicle/create", element: }, @@ -159,7 +166,7 @@ const router = createBrowserRouter([ { path: "vehicle/:id/edit", element: }, // Article { path: "article", element: }, - + // { path: "article/:id", element: }, // { path: "media/create", element: }, ], }, diff --git a/src/entities/navigation/ui/index.tsx b/src/entities/navigation/ui/index.tsx index 77247ea..0091637 100644 --- a/src/entities/navigation/ui/index.tsx +++ b/src/entities/navigation/ui/index.tsx @@ -8,7 +8,7 @@ import List from "@mui/material/List"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import type { NavigationItem } from "../model"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; interface NavigationItemProps { item: NavigationItem; @@ -25,8 +25,11 @@ export const NavigationItemComponent: React.FC = ({ }) => { const Icon = item.icon; const navigate = useNavigate(); + const location = useLocation(); const [isExpanded, setIsExpanded] = React.useState(false); + const isActive = item.path ? location.pathname.startsWith(item.path) : false; + const handleClick = () => { if (item.nestedItems) { setIsExpanded(!isExpanded); @@ -57,6 +60,12 @@ export const NavigationItemComponent: React.FC = ({ isNested && { pl: 4, }, + isActive && { + backgroundColor: "rgba(0, 0, 0, 0.08)", + "&:hover": { + backgroundColor: "rgba(0, 0, 0, 0.12)", + }, + }, ]} > = ({ { minWidth: 0, justifyContent: "center", + color: isActive ? "primary.main" : "inherit", }, open ? { @@ -86,6 +96,10 @@ export const NavigationItemComponent: React.FC = ({ : { opacity: 0, }, + isActive && { + color: "primary.main", + fontWeight: "bold", + }, ]} /> {item.nestedItems && diff --git a/src/pages/Article/ArticleListPage/index.tsx b/src/pages/Article/ArticleListPage/index.tsx index da2b071..965eaeb 100644 --- a/src/pages/Article/ArticleListPage/index.tsx +++ b/src/pages/Article/ArticleListPage/index.tsx @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { articlesStore, languageStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Trash2, FileText } from "lucide-react"; +import { Trash2, Eye } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { DeleteModal, LanguageSwitcher } from "@widgets"; @@ -31,8 +31,8 @@ export const ArticleListPage = observer(() => { renderCell: (params: GridRenderCellParams) => { return (
- +
+ + +
); }) @@ -1178,7 +1271,7 @@ const MapSightbar: React.FC = ({ }`} onClick={() => handleFeatureClick(lId)} > -
+
= ({

)}
- +
+ + +
); }) @@ -1245,7 +1350,7 @@ const MapSightbar: React.FC = ({ }`} onClick={() => handleFeatureClick(sId)} > -
+
= ({ {sName}
- +
+ + +
); }) @@ -1298,53 +1415,66 @@ const MapSightbar: React.FC = ({ } return ( -
+

Панель управления

-
- {sections.map((s) => ( -
- -
-
- {s.content} +
+ + {s.icon} + + {s.title} +
+ + ▼ + + +
+
+ {s.content} +
-
- ))} + ))} +
+ +
); }; @@ -1355,7 +1485,9 @@ export const MapPage: React.FC = () => { const tooltipRef = useRef(null); const [mapServiceInstance, setMapServiceInstance] = useState(null); - const [isLoading, setIsLoading] = useState(true); + // --- ИЗМЕНЕНИЕ: Разделение состояния загрузки --- + const [isMapLoading, setIsMapLoading] = useState(true); // Для рендеринга карты + const [isDataLoading, setIsDataLoading] = useState(true); // Для загрузки данных с API const [error, setError] = useState(null); const [currentMapMode, setCurrentMapMode] = useState("edit"); const [mapFeatures, setMapFeatures] = useState[]>([]); @@ -1376,12 +1508,38 @@ export const MapPage: React.FC = () => { useEffect(() => { let service: MapService | null = null; if (mapRef.current && tooltipRef.current && !mapServiceInstance) { - setIsLoading(true); + // Изначально оба процесса загрузки активны + setIsMapLoading(true); + setIsDataLoading(true); setError(null); + + // --- ИЗМЕНЕНИЕ: Логика загрузки данных вынесена и управляет своим состоянием --- + const loadInitialData = async (mapService: MapService) => { + console.log("Starting data load..."); + try { + await Promise.all([ + mapStore.getRoutes(), + mapStore.getStations(), + mapStore.getSights(), + ]); + mapService.loadFeaturesFromApi( + mapStore.stations, + mapStore.routes, + mapStore.sights + ); + } catch (e) { + console.error("Failed to load initial map data:", e); + setError("Не удалось загрузить данные для карты."); + } finally { + // Завершаем состояние загрузки данных независимо от результата + setIsDataLoading(false); + } + }; + try { service = new MapService( { ...mapConfig, target: mapRef.current }, - setIsLoading, + setIsMapLoading, // MapService теперь управляет только состоянием загрузки карты setError, setCurrentMapMode, handleFeaturesChange, @@ -1390,23 +1548,7 @@ export const MapPage: React.FC = () => { ); setMapServiceInstance(service); - const loadInitialData = async (mapService: MapService) => { - console.log("Starting data load..."); - setIsLoading(true); - try { - // Замените mockApi на реальные fetch запросы к вашему API - const [routes, stations, sights] = await Promise.all([ - mockApi.getRoutes(), - mockApi.getStations(), - mockApi.getSights(), - ]); - mapService.loadFeaturesFromApi(stations, routes, sights); - } catch (e) { - console.error("Failed to load initial map data:", e); - setError("Не удалось загрузить данные для карты."); - } - }; - + // Запускаем загрузку данных loadInitialData(service); } catch (e: any) { console.error("MapPage useEffect error:", e); @@ -1415,7 +1557,9 @@ export const MapPage: React.FC = () => { e.message || "Неизвестная ошибка" }. Пожалуйста, проверьте консоль.` ); - setIsLoading(false); + // В случае критической ошибки инициализации, завершаем все загрузки + setIsMapLoading(false); + setIsDataLoading(false); } } return () => { @@ -1427,6 +1571,9 @@ export const MapPage: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const showLoader = isMapLoading || isDataLoading; + const showContent = mapServiceInstance && !showLoader && !error; + return (
@@ -1441,18 +1588,18 @@ export const MapPage: React.FC = () => { style={{ position: "absolute", pointerEvents: "none", - visibility: "hidden", }} >
- {isLoading && ( + {/* --- ИЗМЕНЕНИЕ: Обновленный лоадер --- */} + {showLoader && (
- Загрузка карты... + {isMapLoading ? "Загрузка карты..." : "Загрузка данных..."}
)} - {error && !isLoading && ( + {error && !showLoader && (

Произошла ошибка

{error}

@@ -1465,14 +1612,15 @@ export const MapPage: React.FC = () => {
)}
- {mapServiceInstance && !isLoading && !error && ( + {/* --- ИЗМЕНЕНИЕ: Условие для отображения контента --- */} + {showContent && ( )}
- {mapServiceInstance && !isLoading && !error && ( + {showContent && ( { + 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, + })); + + this.routes = mappedRoutes; + }; + + getStations = async () => { + const stations = await languageInstance("ru").get("/station"); + const mappedStations = stations.data.map((station: any) => ({ + id: station.id, + name: station.name, + latitude: station.latitude, + longitude: station.longitude, + })); + + this.stations = mappedStations; + }; + + getSights = async () => { + const sights = await languageInstance("ru").get("/sight"); + const mappedSights = sights.data.map((sight: any) => ({ + id: sight.id, + name: sight.name, + description: sight.description, + latitude: sight.latitude, + longitude: sight.longitude, + })); + + this.sights = mappedSights; + }; + + deleteRecourse = async (recourse: string, id: number) => { + await languageInstance("ru").delete(`/${recourse}/${id}`); + if (recourse === "route") { + this.routes = this.routes.filter((route) => route.id !== id); + } else if (recourse === "station") { + this.stations = this.stations.filter((station) => station.id !== id); + } else if (recourse === "sight") { + this.sights = this.sights.filter((sight) => sight.id !== id); + } + }; + + handleSave = async (json: string) => { + const sights: any[] = []; + const routes: any[] = []; + const stations: any[] = []; + + const parsedJSON = JSON.parse(json); + + console.log(parsedJSON); + parsedJSON.features.forEach((feature: any) => { + const { geometry, properties, id } = feature; + const idCanBeSplited = id.split("-"); + + if (!(idCanBeSplited.length > 1)) { + if (properties.featureType === "station") { + stations.push({ + name: properties.name || "", + latitude: geometry.coordinates[1], + longitude: geometry.coordinates[0], + }); + } else if (properties.featureType === "route") { + routes.push({ + route_number: properties.name || "", + path: geometry.coordinates, + }); + } else if (properties.featureType === "sight") { + sights.push({ + name: properties.name || "", + description: properties.description || "", + latitude: geometry.coordinates[1], + longitude: geometry.coordinates[0], + }); + } + } + }); + + for (const station of stations) { + await languageInstance("ru").post("/station", station); + } + for (const route of routes) { + await languageInstance("ru").post("/route", route); + } + for (const sight of sights) { + await languageInstance("ru").post("/sight", sight); + } + }; +} + +export default new MapStore(); diff --git a/src/pages/Media/MediaEditPage/index.tsx b/src/pages/Media/MediaEditPage/index.tsx index 1633a24..74b0949 100644 --- a/src/pages/Media/MediaEditPage/index.tsx +++ b/src/pages/Media/MediaEditPage/index.tsx @@ -164,7 +164,7 @@ export const MediaEditPage = observer(() => { diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx index 90b87a9..2bca4e2 100644 --- a/src/pages/Route/RouteCreatePage/index.tsx +++ b/src/pages/Route/RouteCreatePage/index.tsx @@ -6,20 +6,94 @@ import { MenuItem, FormControl, InputLabel, + Typography, + Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Loader2, Save } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; +import { carrierStore } from "../../../shared/store/CarrierStore"; +import { articlesStore } from "../../../shared/store/ArticlesStore"; +import { Route, routeStore } from "../../../shared/store/RouteStore"; export const RouteCreatePage = observer(() => { const navigate = useNavigate(); + const [carrier, setCarrier] = useState(""); const [routeNumber, setRouteNumber] = useState(""); - const [direction, setDirection] = useState(""); + const [routeCoords, setRouteCoords] = useState(""); + const [govRouteNumber, setGovRouteNumber] = useState(""); + const [governorAppeal, setGovernorAppeal] = useState(""); + const [direction, setDirection] = useState("backward"); + const [scaleMin, setScaleMin] = useState(""); + const [scaleMax, setScaleMax] = useState(""); + const [turn, setTurn] = useState(""); + const [centerLat, setCenterLat] = useState(""); + const [centerLng, setCenterLng] = useState(""); const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + carrierStore.getCarriers(); + articlesStore.getArticleList(); + }, []); + + const handleCreateRoute = async () => { + try { + setIsLoading(true); + // Преобразуем значения в нужные типы + const carrier_id = Number(carrier); + const governor_appeal = Number(governorAppeal); + const scale_min = scaleMin ? Number(scaleMin) : undefined; + const scale_max = scaleMax ? Number(scaleMax) : undefined; + const rotate = turn ? Number(turn) : undefined; + const center_latitude = centerLat ? Number(centerLat) : undefined; + const center_longitude = centerLng ? Number(centerLng) : undefined; + const route_direction = direction === "forward"; + // Координаты маршрута как массив массивов чисел + const path = routeCoords + .split("\n") + .map((line) => + line + .split(" ") + .map((coord) => Number(coord.trim())) + .filter((n) => !isNaN(n)) + ) + .filter((arr) => arr.length === 2); + + // Собираем объект маршрута + const newRoute: Partial = { + carrier: + carrierStore.carriers.data.find((c: any) => c.id === carrier_id) + ?.full_name || "", + carrier_id, + route_number: routeNumber, + route_sys_number: govRouteNumber, + governor_appeal, + route_direction, + scale_min, + scale_max, + rotate, + center_latitude, + center_longitude, + path, + }; + + // Отправка на сервер (пример, если есть routeStore.createRoute) + let createdRoute: Route | null = null; + + await routeStore.createRoute(newRoute); + toast.success("Маршрут успешно создан"); + navigate(-1); + } catch (error) { + console.error(error); + toast.error("Произошла ошибка при создании маршрута"); + } finally { + setIsLoading(false); + } + }; + return (
@@ -28,11 +102,31 @@ export const RouteCreatePage = observer(() => { onClick={() => navigate(-1)} > - Назад + Маршруты / Создать
-

Создание маршрута

-
+ + Создать маршрут + + + + Выберите перевозчика + + { value={routeNumber} onChange={(e) => setRouteNumber(e.target.value)} /> - - Направление + setRouteCoords(e.target.value)} + /> + setGovRouteNumber(e.target.value)} + /> + + Обращение губернатора - + + Прямой/обратный маршрут + + + setScaleMin(e.target.value)} + /> + setScaleMax(e.target.value)} + /> + setTurn(e.target.value)} + /> + setCenterLat(e.target.value)} + /> + setCenterLng(e.target.value)} + /> + +
{/* +
+ +
+ + setLanguageEditStationData(language, { + name: e.target.value, + }) + } + /> + + + Прямой/обратный маршрут + + + + + setLanguageEditStationData(language, { + description: e.target.value, + }) + } + /> + + + setLanguageEditStationData(language, { + address: e.target.value, + }) + } + /> + + { + const [latitude, longitude] = e.target.value.split(" ").map(Number); + if (!isNaN(latitude) && !isNaN(longitude)) { + setEditCommonData({ + latitude: latitude, + longitude: longitude, + }); + } + }} + /> + + + Город + + + + +
+ + ); +}); diff --git a/src/pages/Station/StationListPage/index.tsx b/src/pages/Station/StationListPage/index.tsx index 1174cc5..8bb3f31 100644 --- a/src/pages/Station/StationListPage/index.tsx +++ b/src/pages/Station/StationListPage/index.tsx @@ -2,19 +2,19 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { languageStore, stationsStore } from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Eye, Trash2 } from "lucide-react"; +import { Eye, Pencil, Trash2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets"; export const StationListPage = observer(() => { - const { stations, getStations, deleteStation } = stationsStore; + const { stationLists, getStationList, deleteStation } = stationsStore; const navigate = useNavigate(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [rowId, setRowId] = useState(null); // Lifted state const { language } = languageStore; useEffect(() => { - getStations(); + getStationList(); }, [language]); const columns: GridColDef[] = [ @@ -57,6 +57,9 @@ export const StationListPage = observer(() => { renderCell: (params: GridRenderCellParams) => { return (
+ @@ -74,7 +77,7 @@ export const StationListPage = observer(() => { }, ]; - const rows = stations.map((station) => ({ + const rows = stationLists[language].data.map((station: any) => ({ id: station.id, name: station.name, system_name: station.system_name, diff --git a/src/pages/Station/StationPreviewPage/index.tsx b/src/pages/Station/StationPreviewPage/index.tsx new file mode 100644 index 0000000..e7bb6d6 --- /dev/null +++ b/src/pages/Station/StationPreviewPage/index.tsx @@ -0,0 +1,77 @@ +import { Paper } from "@mui/material"; +import { languageStore, stationsStore } from "@shared"; +import { LanguageSwitcher } from "@widgets"; +import { observer } from "mobx-react-lite"; +import { ArrowLeft } from "lucide-react"; +import { useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; + +export const StationPreviewPage = observer(() => { + const { id } = useParams(); + const { stationPreview, getStationPreview } = stationsStore; + const navigate = useNavigate(); + const { language } = languageStore; + + useEffect(() => { + (async () => { + if (id) { + await getStationPreview(Number(id)); + } + })(); + }, [id, language]); + + return ( + + +
+ +
+
+
+

Название

+

{stationPreview[id!]?.[language]?.data.name}

+
+ +
+

Системное название

+

{stationPreview[id!]?.[language]?.data.system_name}

+
+ +
+

Направление

+

+ {stationPreview[id!]?.[language]?.data.direction + ? "Прямой" + : "Обратный"} +

+
+ + {stationPreview[id!]?.[language]?.data.address && ( +
+

Адрес

+

{stationPreview[id!]?.[language]?.data.address}

+
+ )} + + {stationPreview[id!]?.[language]?.data.description && ( +
+

Описание

+

{stationPreview[id!]?.[language]?.data.description}

+
+ )} +
+
+ ); +}); diff --git a/src/pages/Station/index.ts b/src/pages/Station/index.ts index c1705b8..610bcc7 100644 --- a/src/pages/Station/index.ts +++ b/src/pages/Station/index.ts @@ -1 +1,4 @@ export * from "./StationListPage"; +export * from "./StationCreatePage"; +export * from "./StationPreviewPage"; +export * from "./StationEditPage"; diff --git a/src/pages/User/UserCreatePage/index.tsx b/src/pages/User/UserCreatePage/index.tsx index c641231..219ab77 100644 --- a/src/pages/User/UserCreatePage/index.tsx +++ b/src/pages/User/UserCreatePage/index.tsx @@ -36,7 +36,7 @@ export const UserCreatePage = observer(() => {