feat: cache delete + empty snapshot + route page
This commit is contained in:
@@ -28,7 +28,7 @@ import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||
export const CityCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const { language } = languageStore;
|
||||
const { createCityData, setCreateCityData } = cityStore;
|
||||
const { createCityData, setCreateCityData, setCreateCityWeatherCode } = cityStore;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
@@ -139,6 +139,15 @@ export const CityCreatePage = observer(() => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Код города для погоды"
|
||||
type="number"
|
||||
value={createCityData.weather_city_code ?? 0}
|
||||
helperText="Числовой код города в источнике погоды (Кранштат)"
|
||||
onChange={(e) => setCreateCityWeatherCode(Number(e.target.value))}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||
<ImageUploadCard
|
||||
title="Герб города"
|
||||
|
||||
@@ -40,7 +40,7 @@ export const CityEditPage = observer(() => {
|
||||
>(null);
|
||||
const { language } = languageStore;
|
||||
const { id } = useParams();
|
||||
const { editCityData, editCity, getCity, setEditCityData } = cityStore;
|
||||
const { editCityData, editCity, getCity, setEditCityData, setEditCityWeatherCode } = cityStore;
|
||||
const { getCountries } = countryStore;
|
||||
const { getMedia, getOneMedia } = mediaStore;
|
||||
|
||||
@@ -74,6 +74,7 @@ export const CityEditPage = observer(() => {
|
||||
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||
setEditCityWeatherCode(ruData.weather_city_code ?? 0);
|
||||
|
||||
await getOneMedia(ruData.arms as string);
|
||||
|
||||
@@ -179,6 +180,15 @@ export const CityEditPage = observer(() => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Код города для погоды"
|
||||
type="number"
|
||||
value={editCityData.weather_city_code ?? 0}
|
||||
helperText="Числовой код города в источнике погоды (Кранштат)"
|
||||
onChange={(e) => setEditCityWeatherCode(Number(e.target.value))}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||
<ImageUploadCard
|
||||
title="Герб города"
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CreateRightTab,
|
||||
LeaveAgree,
|
||||
} from "@widgets";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
function a11yProps(index: number) {
|
||||
@@ -28,7 +28,7 @@ export const CreateSightPage = observer(() => {
|
||||
const [value, setValue] = useState(0);
|
||||
const { getCities } = cityStore;
|
||||
const { getArticles } = articlesStore;
|
||||
const { needLeaveAgree } = createSightStore;
|
||||
const needLeave = createSightStore.needLeaveAgree;
|
||||
|
||||
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
|
||||
setValue(newValue);
|
||||
@@ -36,9 +36,15 @@ export const CreateSightPage = observer(() => {
|
||||
|
||||
let blocker = useBlocker(
|
||||
({ currentLocation, nextLocation }) =>
|
||||
needLeaveAgree && currentLocation.pathname !== nextLocation.pathname
|
||||
needLeave && currentLocation.pathname !== nextLocation.pathname,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === "blocked" && !needLeave) {
|
||||
blocker.proceed();
|
||||
}
|
||||
}, [blocker.state, needLeave]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!authStore.me) {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { observer } from "mobx-react-lite";
|
||||
import { Map, Pencil, Trash2, Minus, Monitor } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
import { Box, CircularProgress } from "@mui/material";
|
||||
import { Box, CircularProgress, Tooltip } from "@mui/material";
|
||||
|
||||
export const RouteListPage = observer(() => {
|
||||
const { routes, getRoutes, deleteRoute } = routeStore;
|
||||
const { routes, getRoutes, deleteRoute, sightCounts, stationCounts, countsLoading, loadCounts } = routeStore;
|
||||
const { carriers, getCarriers } = carrierStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@@ -38,6 +38,9 @@ export const RouteListPage = observer(() => {
|
||||
await getCarriers("zh");
|
||||
await getRoutes();
|
||||
setIsLoading(false);
|
||||
|
||||
const routeIds = routeStore.routes.data.map((r) => r.id);
|
||||
loadCounts(routeIds);
|
||||
};
|
||||
fetchData();
|
||||
}, [language]);
|
||||
@@ -128,6 +131,42 @@ export const RouteListPage = observer(() => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "sightCount",
|
||||
headerName: "Достопримечательности",
|
||||
width: 180,
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: true,
|
||||
renderHeader: (params: any) => (
|
||||
<Tooltip title="Количество привязанных достопримечательностей">
|
||||
<span>{params.colDef.headerName}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
{params.value === null ? <CircularProgress size={14} /> : params.value}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "stationCount",
|
||||
headerName: "Остановки",
|
||||
width: 120,
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: true,
|
||||
renderHeader: (params: any) => (
|
||||
<Tooltip title="Количество привязанных остановок">
|
||||
<span>{params.colDef.headerName}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
{params.value === null ? <CircularProgress size={14} /> : params.value}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...(canShowActionsColumn ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
@@ -195,8 +234,10 @@ export const RouteListPage = observer(() => {
|
||||
route_sys_number: route.route_sys_number,
|
||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||
route_name: route.route_name,
|
||||
sightCount: sightCounts.has(route.id) ? sightCounts.get(route.id) : null,
|
||||
stationCount: stationCounts.has(route.id) ? stationCounts.get(route.id) : null,
|
||||
}));
|
||||
}, [routes.data, carriers["ru"].data, selectedCityStore.selectedCityId, searchQuery]);
|
||||
}, [routes.data, carriers["ru"].data, selectedCityStore.selectedCityId, searchQuery, sightCounts.size, stationCounts.size, countsLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1910,7 +1910,8 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
|
||||
const stationIconSizePercent =
|
||||
liveStationIconSizes.get(station.id) ??
|
||||
(typeof station.icon_size === "number" && Number.isFinite(station.icon_size)
|
||||
(typeof station.icon_size === "number" &&
|
||||
Number.isFinite(station.icon_size)
|
||||
? station.icon_size
|
||||
: 100);
|
||||
const iconSizePx = Math.max(
|
||||
@@ -2277,6 +2278,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
pointerEvents: "none",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{stationData.ru.map((station, index) => {
|
||||
@@ -2706,13 +2708,14 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
? camera.scale /
|
||||
Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6)
|
||||
: 1;
|
||||
const sightIconSizePercent = sight.is_default_icon === false
|
||||
? (liveSightIconSizes.get(sight.id) ??
|
||||
(typeof sight.icon_size === "number" &&
|
||||
Number.isFinite(sight.icon_size)
|
||||
? sight.icon_size
|
||||
: 100))
|
||||
: (routeData?.icon_size ?? originalRouteData?.icon_size ?? 100);
|
||||
const sightIconSizePercent =
|
||||
sight.is_default_icon === false
|
||||
? (liveSightIconSizes.get(sight.id) ??
|
||||
(typeof sight.icon_size === "number" &&
|
||||
Number.isFinite(sight.icon_size)
|
||||
? sight.icon_size
|
||||
: 100))
|
||||
: (routeData?.icon_size ?? originalRouteData?.icon_size ?? 100);
|
||||
const iconSize =
|
||||
30 *
|
||||
clamp(sightIconSizePercent / 100, 0.1, 10) *
|
||||
@@ -2723,7 +2726,10 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
resizingSightIconId === sight.id);
|
||||
const iconLeft = cssX - iconSize;
|
||||
const iconTop = cssY - iconSize;
|
||||
const sightZoomClampedScale = Math.min(Math.max(camera.scale, 1), 3);
|
||||
const sightZoomClampedScale = Math.min(
|
||||
Math.max(camera.scale, 1),
|
||||
3,
|
||||
);
|
||||
const sightScaleFactor = 1 + (sightZoomClampedScale - 1) * 0.4;
|
||||
const labelHeight = 24 * sightScaleFactor;
|
||||
const labelPadding = 6 * sightScaleFactor;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useState, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DatabaseBackup, Trash2 } from "lucide-react";
|
||||
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
||||
import { Alert, Box, CircularProgress } from "@mui/material";
|
||||
import { Alert, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from "@mui/material";
|
||||
|
||||
const LOW_STORAGE_THRESHOLD_GB = 10;
|
||||
|
||||
@@ -30,6 +30,7 @@ export const SnapshotListPage = observer(() => {
|
||||
restoreSnapshot,
|
||||
storageInfo,
|
||||
getStorageInfo,
|
||||
createEmptySnapshot,
|
||||
} = snapshotStore;
|
||||
const canWriteDevices = authStore.canWrite("devices");
|
||||
const canCreateSnapshot =
|
||||
@@ -42,6 +43,9 @@ export const SnapshotListPage = observer(() => {
|
||||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isEmptySnapshotModalOpen, setIsEmptySnapshotModalOpen] = useState(false);
|
||||
const [emptySnapshotName, setEmptySnapshotName] = useState("");
|
||||
const [isCreatingEmpty, setIsCreatingEmpty] = useState(false);
|
||||
const [paginationModel, setPaginationModel] = useState({
|
||||
page: 0,
|
||||
pageSize: 50,
|
||||
@@ -167,13 +171,27 @@ export const SnapshotListPage = observer(() => {
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
||||
|
||||
{canCreateSnapshot && (
|
||||
<CreateButton
|
||||
label="Создать экспорт медиа"
|
||||
path="/snapshot/create"
|
||||
disabled={isLowStorage}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
{canCreateSnapshot && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={isLowStorage}
|
||||
onClick={() => {
|
||||
setEmptySnapshotName("");
|
||||
setIsEmptySnapshotModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Создать пустой снапшот
|
||||
</Button>
|
||||
)}
|
||||
{canCreateSnapshot && (
|
||||
<CreateButton
|
||||
label="Создать экспорт медиа"
|
||||
path="/snapshot/create"
|
||||
disabled={isLowStorage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{usedGB != null && totalGB != null && (
|
||||
<div className="bg-white rounded-2xl p-5 mb-6 shadow-sm border border-gray-100">
|
||||
@@ -301,6 +319,46 @@ export const SnapshotListPage = observer(() => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={isEmptySnapshotModalOpen}
|
||||
onClose={() => setIsEmptySnapshotModalOpen(false)}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>Создать пустой снапшот</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Название"
|
||||
value={emptySnapshotName}
|
||||
onChange={(e) => setEmptySnapshotName(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setIsEmptySnapshotModalOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!emptySnapshotName.trim() || isCreatingEmpty}
|
||||
onClick={async () => {
|
||||
setIsCreatingEmpty(true);
|
||||
try {
|
||||
await createEmptySnapshot(emptySnapshotName);
|
||||
await getSnapshots();
|
||||
setIsEmptySnapshotModalOpen(false);
|
||||
} finally {
|
||||
setIsCreatingEmpty(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCreatingEmpty ? <CircularProgress size={20} /> : "Создать"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<SnapshotRestore
|
||||
open={isRestoreModalOpen}
|
||||
loading={isLoading}
|
||||
|
||||
Reference in New Issue
Block a user