feat: update demo page + add city_id for media and articles

This commit is contained in:
2026-04-28 10:57:42 +03:00
parent 60c6840db4
commit 94f512e0e4
27 changed files with 499 additions and 222 deletions

View File

@@ -1,6 +1,6 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, articlesStore, languageStore, SearchInput } from "@shared";
import { authStore, articlesStore, languageStore, SearchInput, selectedCityStore } from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Trash2, Eye, Minus } from "lucide-react";
@@ -75,14 +75,16 @@ export const ArticleListPage = observer(() => {
const rows = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
const cityId = selectedCityStore.selectedCityId;
return articleList[language].data
.filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query))
.filter((article) => !cityId || article.city_id === cityId)
.map((article) => ({
id: article.id,
heading: article.heading,
body: article.body,
}));
}, [articleList[language].data, searchQuery]);
}, [articleList[language].data, searchQuery, selectedCityStore.selectedCityId]);
return (
<>

View File

@@ -28,7 +28,8 @@ import { LanguageSwitcher, ImageUploadCard } from "@widgets";
export const CityCreatePage = observer(() => {
const navigate = useNavigate();
const { language } = languageStore;
const { createCityData, setCreateCityData, setCreateCityWeatherCode } = cityStore;
const { createCityData, setCreateCityData, setCreateCityWeatherCode } =
cityStore;
const [isLoading, setIsLoading] = useState(false);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
@@ -72,7 +73,7 @@ export const CityCreatePage = observer(() => {
createCityData[language].name,
createCityData.country_code,
media.id,
language
language,
);
};
@@ -111,7 +112,7 @@ export const CityCreatePage = observer(() => {
e.target.value,
createCityData.country_code,
createCityData.arms,
language
language,
)
}
/>
@@ -127,7 +128,7 @@ export const CityCreatePage = observer(() => {
createCityData[language].name,
e.target.value,
createCityData.arms,
language
language,
);
}}
>
@@ -144,7 +145,6 @@ export const CityCreatePage = observer(() => {
label="Код города для погоды"
type="number"
value={createCityData.weather_city_code ?? 0}
helperText="Числовой код города в источнике погоды (Кранштат)"
onChange={(e) => setCreateCityWeatherCode(Number(e.target.value))}
/>
@@ -162,7 +162,7 @@ export const CityCreatePage = observer(() => {
createCityData[language].name,
createCityData.country_code,
"",
language
language,
);
setActiveMenuType(null);
}}

View File

@@ -40,7 +40,13 @@ export const CityEditPage = observer(() => {
>(null);
const { language } = languageStore;
const { id } = useParams();
const { editCityData, editCity, getCity, setEditCityData, setEditCityWeatherCode } = cityStore;
const {
editCityData,
editCity,
getCity,
setEditCityData,
setEditCityWeatherCode,
} = cityStore;
const { getCountries } = countryStore;
const { getMedia, getOneMedia } = mediaStore;
@@ -108,7 +114,7 @@ export const CityEditPage = observer(() => {
: null;
const effectiveArmsUrl = isMediaIdEmpty(editCityData.arms)
? null
: selectedMedia?.id ?? editCityData.arms;
: (selectedMedia?.id ?? editCityData.arms);
if (isLoadingData) {
return (
@@ -185,7 +191,6 @@ export const CityEditPage = observer(() => {
label="Код города для погоды"
type="number"
value={editCityData.weather_city_code ?? 0}
helperText="Числовой код города в источнике погоды (Кранштат)"
onChange={(e) => setEditCityWeatherCode(Number(e.target.value))}
/>

View File

@@ -12,7 +12,7 @@ import {
CreateRightTab,
LeaveAgree,
} from "@widgets";
import { useEffect, useState, useRef } from "react";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
function a11yProps(index: number) {

View File

@@ -7,7 +7,7 @@ import {
FormControl,
InputLabel,
} from "@mui/material";
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
import { mediaStore, MEDIA_TYPE_LABELS, selectedCityStore } from "@shared";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useState } from "react";
@@ -23,7 +23,7 @@ export const MediaCreatePage = observer(() => {
const handleCreate = async () => {
try {
setIsLoading(true);
await mediaStore.createMedia(name, type);
await mediaStore.createMedia(name, type, selectedCityStore.selectedCityId);
toast.success("Медиа успешно создано");
navigate("/media");
} catch (error) {

View File

@@ -1,6 +1,6 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput } from "@shared";
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput, selectedCityStore } from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2, Minus } from "lucide-react";
@@ -98,14 +98,16 @@ export const MediaListPage = observer(() => {
const rows = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
const cityId = selectedCityStore.selectedCityId;
return media
.filter((item) => !query || (item.media_name ?? "").toLowerCase().includes(query))
.filter((item) => !cityId || item.city_id === cityId)
.map((item) => ({
id: item.id,
media_name: item.media_name,
media_type: item.media_type,
}));
}, [media, searchQuery]);
}, [media, searchQuery, selectedCityStore.selectedCityId]);
return (
<>

View File

@@ -286,9 +286,9 @@ export const RouteCreatePage = observer(() => {
newRoute.governor_appeal = governor_appeal;
}
await routeStore.createRoute(newRoute);
const newId = await routeStore.createRoute(newRoute);
toast.success("Маршрут успешно создан");
navigate(-1);
navigate(`/route/${newId}/edit`);
} catch (error) {
console.error(error);
toast.error("Произошла ошибка при создании маршрута");

View File

@@ -141,6 +141,7 @@ export function RightSidebar() {
bgcolor="primary.main"
border="1px solid #e0e0e0"
borderRadius={2}
zIndex={2}
>
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
Настройка маршрута

View File

@@ -2,6 +2,7 @@ import { Stack, Typography, Box, IconButton } from "@mui/material";
import { Close } from "@mui/icons-material";
import { Landmark } from "lucide-react";
import { useMapData } from "./MapDataContext";
import { RouteWidget } from "./webgl-prototype/RouteWidget";
export function Widgets() {
const { selectedSight, setSelectedSight } = useMapData();
@@ -13,22 +14,11 @@ export function Widgets() {
position="absolute"
top={32}
left={32}
zIndex={2}
sx={{ pointerEvents: "none" }}
>
<Stack
bgcolor="primary.main"
width={361}
height={96}
p={2}
m={2}
borderRadius={2}
alignItems="center"
justifyContent="center"
>
<Typography variant="h6" sx={{ color: "#fff" }}>
Остановка
</Typography>
</Stack>
{/* Виджет маршрута */}
<RouteWidget />
{/* Виджет выбранной достопримечательности (заменяет виджет погоды) */}
<Stack

View File

@@ -0,0 +1,105 @@
.route-widget-label.marquee {
display: inline-block;
animation: marquee 14s linear infinite;
}
.route-widget-subtitle.marquee {
display: inline-block;
animation: marquee 14s linear infinite;
}
@keyframes marquee {
0% {
transform: translateX(0);
}
50% {
transform: translateX(-50%);
}
100% {
transform: translateX(0);
}
}
.route-widget {
width: 361px;
height: 96px;
position: fixed;
display: inline-flex;
border-radius: 10px;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset,
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
padding: 1px; /* Чтобы контент не прилипал к рамке */
background: linear-gradient(
to bottom right,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(179, 165, 152, 0.4);
backdrop-filter: blur(10px);
pointer-events: auto;
z-index: 10000001;
}
.route-widget-number {
position: absolute;
width: fit-content;
left: 0px;
top: 0px;
min-width: 94px;
max-width: 100px;
height: 96px;
background-color: #fcd500;
color: black;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 70px;
padding: 14px;
font-weight: 900;
}
.route-widget-content {
overflow: hidden;
width: 257px;
display: flex;
flex-direction: column;
margin-top: 13px;
margin-left: 109px;
margin-right: 9px;
}
.route-widget-label {
white-space: nowrap;
font-size: 24px;
margin: 1px 0;
font-style: normal;
font-weight: 700;
line-height: 24px;
color: white;
}
.route-widget-label--medium {
font-size: 22px;
line-height: 22px;
}
.route-widget-label--small {
font-size: 20px;
line-height: 20px;
}
.route-widget-label--xsmall {
font-size: 18px;
line-height: 18px;
}
.route-widget-subtitle {
white-space: nowrap;
color: #cbcbcb;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-top: 4px;
}

View File

@@ -0,0 +1,72 @@
import styles from "./RouteWidget.module.css";
import { useMapData } from "../MapDataContext";
import { languageStore } from "@shared";
import { observer } from "mobx-react-lite";
const shouldAnimate = (text: string | undefined, maxLength: number) =>
(text?.length ?? 0) > maxLength;
const getLabelSizeClass = (text: string | undefined) => {
const length = text?.length ?? 0;
if (length <= 40) return "";
if (length <= 60) return styles["route-widget-label--medium"];
if (length <= 80) return styles["route-widget-label--small"];
return styles["route-widget-label--xsmall"];
};
export const RouteWidget = observer(() => {
const { routeData, stationData } = useMapData();
const { language } = languageStore;
const stations = stationData?.[language] ?? stationData?.["ru"] ?? [];
const stationsRu = stationData?.["ru"] ?? [];
const startStation = stations[0];
const endStation = stations[stations.length - 1];
const startStationRu = stationsRu[0];
const endStationRu = stationsRu[stationsRu.length - 1];
const enSubtitle = `${startStationRu?.name ?? ""} - ${endStationRu?.name ?? ""}`;
const zhSubtitle = `${startStation?.name ?? ""} - ${endStation?.name ?? ""}`;
const subtitle = language === "zh" ? zhSubtitle : enSubtitle;
return (
<div className={styles["route-widget"]} style={{ position: "relative" }}>
<div className={styles["route-widget-number"]}>
{routeData?.route_sys_number || ""}
</div>
<div className={styles["route-widget-content"]}>
<div
className={[
styles["route-widget-label"],
shouldAnimate(startStation?.name, 18) ? styles["marquee"] : "",
getLabelSizeClass(startStation?.name),
].join(" ")}
title={startStation?.name}
>
{startStation?.name}
</div>
<div
className={[
styles["route-widget-label"],
shouldAnimate(endStation?.name, 18) ? styles["marquee"] : "",
getLabelSizeClass(endStation?.name),
].join(" ")}
title={endStation?.name}
>
{endStation?.name}
</div>
<div
className={[
styles["route-widget-subtitle"],
shouldAnimate(subtitle, 50) ? styles["marquee"] : "",
].join(" ")}
title={subtitle}
>
{subtitle}
</div>
</div>
</div>
);
});

View File

@@ -68,9 +68,9 @@ export const StationCreatePage = observer(() => {
const executeCreate = async () => {
try {
setIsLoading(true);
await createStation();
const data = await createStation();
toast.success("Остановка успешно создана");
navigate("/station");
navigate(`/station/${data.id}/edit`);
} catch (error) {
console.error("Error creating station:", error);
toast.error("Ошибка при создании остановки");

View File

@@ -326,6 +326,9 @@ export const UserEditPage = observer(() => {
if (!next.includes("snapshot_create")) {
next.push("snapshot_create");
}
if (!next.includes("devices_maintenance_rw")) {
next.push("devices_maintenance_rw");
}
next.push("admin");
return next;
});
@@ -347,7 +350,7 @@ export const UserEditPage = observer(() => {
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение</TableCell>
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение/Запись</TableCell>
<TableCell align="center" sx={{ fontWeight: 600 }}>
Создание (snapshot_create)
Доп. права
</TableCell>
</TableRow>
</TableHead>
@@ -386,6 +389,8 @@ export const UserEditPage = observer(() => {
});
};
const isDevicesResource = key === "devices";
const handleSnapshotCreateChange = (checked: boolean) => {
if (!isSnapshotResource) {
return;
@@ -400,6 +405,13 @@ export const UserEditPage = observer(() => {
});
};
const handleMaintenanceChange = (checked: boolean) => {
setLocalRoles((prev) => {
const without = prev.filter((r) => r !== "devices_maintenance_rw");
return checked ? [...without, "devices_maintenance_rw"] : without;
});
};
return (
<TableRow key={key} hover>
<TableCell>{label}</TableCell>
@@ -447,6 +459,14 @@ export const UserEditPage = observer(() => {
handleSnapshotCreateChange(e.target.checked)
}
size="small"
title="Разрешает создавать новые снапшоты"
/>
) : isDevicesResource ? (
<Checkbox
checked={localRoles.includes("devices_maintenance_rw")}
onChange={(e) => handleMaintenanceChange(e.target.checked)}
size="small"
title="Разрешает переводить устройства в режим технического обслуживания"
/>
) : (
<Typography variant="body2" color="text.secondary">