feat: update demo page + add city_id for media and articles
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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))}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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("Произошла ошибка при создании маршрута");
|
||||
|
||||
@@ -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">
|
||||
Настройка маршрута
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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("Ошибка при создании остановки");
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user