Compare commits
4 Commits
83ccdef790
...
7e539f550b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e539f550b | |||
| fbf6b0dc9d | |||
| a997cdb198 | |||
| bf45dcdbfc |
@@ -37,14 +37,14 @@ const Fullscreen3DModal = ({ isOpen, onClose, fileUrl }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="fullscreen-3d-actions">
|
||||
<button title="Увеличить масштаб" disabled={scale >= 1}>
|
||||
<button disabled={scale >= 1}>
|
||||
<img src={scale_plus} alt="Увеличить" />
|
||||
</button>
|
||||
<button title="Уменьшить масштаб" disabled={scale <= 0.1}>
|
||||
<button disabled={scale <= 0.1}>
|
||||
<img src={scale_minus} alt="Уменьшить" />
|
||||
</button>
|
||||
|
||||
<button onPointerUp={onClose} title="Закрыть">
|
||||
<button onPointerUp={onClose}>
|
||||
<img src={closeIcon} alt="Закрыть" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const SightComponent = function SightComponent({
|
||||
aria-label={`Выбрать достопримечательность ${title}`}
|
||||
>
|
||||
<div className="sight-image">{renderThumbnail()}</div>
|
||||
<div className="sight-title" title={title}>
|
||||
<div className="sight-title">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -361,7 +361,6 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
||||
<button
|
||||
type="button"
|
||||
className="three-d-control-btn"
|
||||
title="Уменьшить"
|
||||
onPointerUp={() => threeViewControlRef.current?.zoomOut?.()}
|
||||
>
|
||||
<MinusIcon />
|
||||
@@ -369,7 +368,6 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
||||
<button
|
||||
type="button"
|
||||
className="three-d-control-btn"
|
||||
title="Увеличить"
|
||||
onPointerUp={() => threeViewControlRef.current?.zoomIn?.()}
|
||||
>
|
||||
<PlusIcon />
|
||||
@@ -377,7 +375,6 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
||||
<button
|
||||
type="button"
|
||||
className="three-d-control-btn"
|
||||
title={isFullscreen3D ? "Свернуть" : "Развернуть"}
|
||||
onPointerUp={() => {
|
||||
if (isFullscreen3D) {
|
||||
setIsFullscreen3D(false);
|
||||
@@ -392,11 +389,6 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
||||
</div>
|
||||
<button
|
||||
className={"fullscreen-3d-button"}
|
||||
title={
|
||||
isFullscreen3D
|
||||
? "Закрыть полноэкранный режим"
|
||||
: "Открыть в полноэкранном режиме"
|
||||
}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -2932,7 +2932,6 @@ export const WebGLMap = observer(() => {
|
||||
whiteSpace: "nowrap",
|
||||
flex: 1,
|
||||
}}
|
||||
title={sightName}
|
||||
>
|
||||
{sightName}
|
||||
</span>
|
||||
|
||||
@@ -123,6 +123,8 @@ const LeftWidget = observer(
|
||||
? routeSightsEn.find((sight) => sight.id === selectedSightId)
|
||||
: routeSightsZh.find((sight) => sight.id === selectedSightId);
|
||||
|
||||
if (!sight) return;
|
||||
|
||||
const leftArticle = sight.left_article;
|
||||
|
||||
const leftArticleData =
|
||||
@@ -132,6 +134,8 @@ const LeftWidget = observer(
|
||||
? sightArticlesEn.get(leftArticle + "_" + selectedLanguage)
|
||||
: sightArticlesZh.get(leftArticle + "_" + selectedLanguage);
|
||||
|
||||
if (!leftArticleData?.media?.length) return;
|
||||
|
||||
const media = await ContentAPI.getMediaPreview(
|
||||
leftArticleData.media[0].id,
|
||||
selectedLanguage,
|
||||
|
||||
@@ -505,7 +505,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
||||
: selectedLanguage == "zh"
|
||||
? "景点"
|
||||
: "Attractions"}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onPointerUp={() => {
|
||||
if (!isStationOpen) {
|
||||
@@ -553,7 +553,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
||||
: selectedLanguage == "zh"
|
||||
? "车站"
|
||||
: "Stations"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="side-menu-tag">
|
||||
{/* {selectedLanguage == "ru"
|
||||
|
||||
@@ -64,7 +64,6 @@ const StationItem = ({
|
||||
className="side-menu-sight"
|
||||
onPointerDown={(e) => handlePointerDown(e, station.id)}
|
||||
onPointerUp={(e) => handlePointerUp(e, station.id, handleStationClick)}
|
||||
title={station.name}
|
||||
>
|
||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||
{station.name}
|
||||
|
||||
@@ -105,7 +105,6 @@ const SightItem = ({
|
||||
className={`side-menu-sight pointer ${
|
||||
localSelectedSightId === sight.id ? "selected" : ""
|
||||
}`}
|
||||
title={sightName}
|
||||
>
|
||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||
{sightName}
|
||||
|
||||
@@ -70,7 +70,6 @@ const SightItem = ({
|
||||
className={`side-menu-sight pointer ${
|
||||
localSelectedSightId === sight.id ? "selected" : ""
|
||||
}`}
|
||||
title={sightName}
|
||||
>
|
||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||
{sightName}
|
||||
|
||||
@@ -124,7 +124,6 @@ const StationItem = ({
|
||||
);
|
||||
}
|
||||
}}
|
||||
title={station.name}
|
||||
>
|
||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||
{station.name}
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import '../../styles/AppealWidget.css'
|
||||
import { TouchableLayout } from '../TouchableLayout'
|
||||
import { useRef, useEffect } from "react";
|
||||
import "../../styles/AppealWidget.css";
|
||||
import { TouchableLayout } from "../TouchableLayout";
|
||||
|
||||
function AppealWidget({widgetImgPath, widgetLabel, widgetText, style, isOpen}) {
|
||||
const stopProp = (e) => { e.stopPropagation(); e.preventDefault(); };
|
||||
function AppealWidget({
|
||||
widgetImgPath,
|
||||
widgetLabel,
|
||||
widgetText,
|
||||
style,
|
||||
isOpen,
|
||||
}) {
|
||||
const stopProp = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
const layoutRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && layoutRef.current) {
|
||||
const scrollable = layoutRef.current.querySelector('.scrollable');
|
||||
const scrollable = layoutRef.current.querySelector(".scrollable");
|
||||
if (scrollable) scrollable.scrollTop = 0;
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div style={style} className='dynamic-widget'
|
||||
<div
|
||||
style={style}
|
||||
className="dynamic-widget"
|
||||
onPointerDown={stopProp}
|
||||
onPointerMove={stopProp}
|
||||
onPointerUp={stopProp}
|
||||
>
|
||||
{widgetImgPath && <img className='dynamic-widget-image' src={widgetImgPath} />}
|
||||
<div className='dynamic-widget-label'>{widgetLabel}</div>
|
||||
<TouchableLayout ref={layoutRef} className="dynamic-widget-text-scroll" maxHeight="calc(100vh - 150px - 100px - 300px)">
|
||||
<div className='dynamic-widget-text'>{widgetText}</div>
|
||||
{widgetImgPath && (
|
||||
<img className="dynamic-widget-image" src={widgetImgPath} />
|
||||
)}
|
||||
<div className="dynamic-widget-label">{widgetLabel}</div>
|
||||
<TouchableLayout
|
||||
ref={layoutRef}
|
||||
className="dynamic-widget-text-scroll"
|
||||
>
|
||||
<div className="dynamic-widget-text">{widgetText}</div>
|
||||
</TouchableLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppealWidget
|
||||
export default AppealWidget;
|
||||
|
||||
@@ -92,16 +92,14 @@ const RouteWidget = observer(() => {
|
||||
className={`route-widget-label ${
|
||||
shouldAnimate(startStation?.name, 18) ? "marquee" : ""
|
||||
} ${getLabelSizeClass(startStation?.name)}`}
|
||||
title={startStation?.name}
|
||||
>
|
||||
>
|
||||
{startStation?.name}
|
||||
</div>
|
||||
<div
|
||||
className={`route-widget-label ${
|
||||
shouldAnimate(endStation?.name, 18) ? "marquee" : ""
|
||||
} ${getLabelSizeClass(endStation?.name)}`}
|
||||
title={endStation?.name}
|
||||
>
|
||||
>
|
||||
{endStation?.name}
|
||||
</div>
|
||||
{(selectedLanguage === "en" || selectedLanguage === "ru") && (
|
||||
@@ -109,8 +107,7 @@ const RouteWidget = observer(() => {
|
||||
className={`route-widget-subtitle ${
|
||||
shouldAnimate(routeEnSubtitle, 50) ? "marquee" : ""
|
||||
}`}
|
||||
title={routeEnSubtitle}
|
||||
>
|
||||
>
|
||||
{routeEnSubtitle}
|
||||
</div>
|
||||
)}
|
||||
@@ -119,8 +116,7 @@ const RouteWidget = observer(() => {
|
||||
className={`route-widget-subtitle ${
|
||||
shouldAnimate(routeZhSubtitle, 50) ? "marquee" : ""
|
||||
}`}
|
||||
title={routeZhSubtitle}
|
||||
>
|
||||
>
|
||||
{routeZhSubtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 420px;
|
||||
max-height: calc(100vh - 150px - 100px);
|
||||
max-height: calc(100vh - 150px - 98px);
|
||||
border-radius: 10px;
|
||||
background:
|
||||
linear-gradient(
|
||||
@@ -13,49 +13,61 @@
|
||||
rgba(255, 255, 255, 0) 8.71%,
|
||||
rgba(255, 255, 255, 0.16) 69.69%
|
||||
),
|
||||
var(--carrier-left, #006F3A);
|
||||
var(--carrier-left, #006f3a);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.dynamic-widget-image {
|
||||
padding-top: 4px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
width: 412px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
padding-top: 2px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
width: 416px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dynamic-widget-label {
|
||||
width: 380px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid var(--Glass-stroke, rgba(255, 255, 255, 0.8));
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
),
|
||||
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4);
|
||||
}
|
||||
|
||||
.dynamic-widget-text-scroll {
|
||||
margin-top: 0;
|
||||
margin-bottom: 25px;
|
||||
.dynamic-widget-text-scroll.scrollable-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
align-self: stretch;
|
||||
margin: 15px 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dynamic-widget-text-scroll .scrollable-viewport {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dynamic-widget-text-scroll .scrollable {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dynamic-widget-text-scroll.scrollable-container {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.dynamic-widget-text-scroll .scrollable-viewport {
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dynamic-widget-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 190%;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
@@ -55,11 +55,12 @@ export const ArticleListPage = observer(() => {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||
<button title="Просмотр" onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
{canWriteArticles && (
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -107,7 +108,9 @@ export const ArticleListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<div className="w-full">
|
||||
<DataGrid
|
||||
|
||||
@@ -98,10 +98,11 @@ export const CarrierListPage = observer(() => {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -161,7 +162,9 @@ export const CarrierListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -119,10 +119,11 @@ export const CityListPage = observer(() => {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -164,7 +165,9 @@ export const CityListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={filteredRows}
|
||||
|
||||
@@ -117,7 +117,9 @@ export const CountryListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -78,11 +78,12 @@ export const MediaListPage = observer(() => {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<button title="Просмотр" onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
{canWriteMedia && (
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -112,7 +113,9 @@ export const MediaListPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full">
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
{canWriteMedia && ids.length > 0 && (
|
||||
<div className="flex justify-end mb-5 duration-300">
|
||||
|
||||
@@ -178,22 +178,23 @@ export const RouteListPage = observer(() => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
{canWriteRoutes && (
|
||||
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
{canShowRoutePreview && (
|
||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||
<button title="Предпросмотр на карте" onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||
<Map size={20} className="text-purple-500" />
|
||||
</button>
|
||||
)}
|
||||
{canShowRoutePreview && (
|
||||
<button onClick={() => window.open(`/demo/${params.row.id}`, "_blank")}>
|
||||
<button title="Демо" onClick={() => window.open(`/demo/${params.row.id}`, "_blank")}>
|
||||
<Monitor size={20} className="text-green-500" />
|
||||
</button>
|
||||
)}
|
||||
{canWriteRoutes && (
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -270,7 +271,9 @@ export const RouteListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -105,10 +105,11 @@ export const SightListPage = observer(() => {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -181,7 +182,9 @@ export const SightListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Button, TextField } from "@mui/material";
|
||||
import { snapshotStore } from "@shared";
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { snapshotStore, authStore, routeStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -12,6 +19,76 @@ export const SnapshotCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [duplicateWarningOpen, setDuplicateWarningOpen] = useState(false);
|
||||
const [duplicateRouteNumbers, setDuplicateRouteNumbers] = useState<string[]>([]);
|
||||
|
||||
const canReadRoutes = authStore.canRead("routes");
|
||||
|
||||
const startExport = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const id = await createSnapshot(name);
|
||||
|
||||
await getSnapshotStatus(id);
|
||||
|
||||
while (snapshotStore.snapshotStatus?.Status != "done") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await getSnapshotStatus(id);
|
||||
}
|
||||
|
||||
if (snapshotStore.snapshotStatus?.Status === "done") {
|
||||
toast.success("Экспорт медиа успешно создан");
|
||||
|
||||
runInAction(() => {
|
||||
snapshotStore.snapshotStatus = null;
|
||||
});
|
||||
|
||||
await getStorageInfo();
|
||||
navigate(-1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Ошибка при создании экспорта медиа");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!canReadRoutes) {
|
||||
await startExport();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
routeStore.routes.loaded = false;
|
||||
});
|
||||
await routeStore.getRoutes();
|
||||
|
||||
const routes = routeStore.routes.data;
|
||||
const numberCount = new Map<string, number>();
|
||||
for (const route of routes) {
|
||||
const num = (route.route_number ?? "").trim();
|
||||
if (num) {
|
||||
numberCount.set(num, (numberCount.get(num) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const duplicates = Array.from(numberCount.entries())
|
||||
.filter(([, count]) => count > 1)
|
||||
.map(([num]) => num);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
setDuplicateRouteNumbers(duplicates);
|
||||
setDuplicateWarningOpen(true);
|
||||
} else {
|
||||
await startExport();
|
||||
}
|
||||
} catch {
|
||||
await startExport();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-[400px] flex justify-center items-center">
|
||||
@@ -40,35 +117,7 @@ export const SnapshotCreatePage = observer(() => {
|
||||
color="primary"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const id = await createSnapshot(name);
|
||||
|
||||
await getSnapshotStatus(id);
|
||||
|
||||
while (snapshotStore.snapshotStatus?.Status != "done") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await getSnapshotStatus(id);
|
||||
}
|
||||
|
||||
if (snapshotStore.snapshotStatus?.Status === "done") {
|
||||
toast.success("Экспорт медиа успешно создан");
|
||||
|
||||
runInAction(() => {
|
||||
snapshotStore.snapshotStatus = null;
|
||||
});
|
||||
|
||||
await getStorageInfo();
|
||||
navigate(-1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Ошибка при создании экспорта медиа");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || !name.trim()}
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -87,6 +136,47 @@ export const SnapshotCreatePage = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={duplicateWarningOpen}
|
||||
onClose={() => !isLoading && setDuplicateWarningOpen(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Найдены повторяющиеся маршруты</DialogTitle>
|
||||
<DialogContent>
|
||||
<p className="mb-3">
|
||||
Обнаружены маршруты с одинаковыми номерами. Это может привести к
|
||||
некорректным данным в экспорте.
|
||||
</p>
|
||||
<ul className="list-disc pl-5">
|
||||
{duplicateRouteNumbers.map((num) => (
|
||||
<li key={num}>
|
||||
Найдены повторяющиеся маршруты под номером №{num}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => setDuplicateWarningOpen(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ backgroundColor: "#795548", "&:hover": { backgroundColor: "#5D4037" } }}
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setDuplicateWarningOpen(false);
|
||||
await startExport();
|
||||
}}
|
||||
>
|
||||
Продолжить экспорт
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -137,6 +137,7 @@ export const SnapshotListPage = observer(() => {
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button
|
||||
title="Восстановить"
|
||||
onClick={() => {
|
||||
setIsRestoreModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -145,6 +146,7 @@ export const SnapshotListPage = observer(() => {
|
||||
<DatabaseBackup size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -297,7 +299,9 @@ export const SnapshotListPage = observer(() => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -141,7 +141,7 @@ export const StationListPage = observer(() => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
{canWriteStations && (
|
||||
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
@@ -151,13 +151,14 @@ export const StationListPage = observer(() => {
|
||||
setSelectedStationId(params.row.id);
|
||||
setIsTransfersModalOpen(true);
|
||||
}}
|
||||
title="Редактировать пересадки"
|
||||
title="Управление пересадками"
|
||||
>
|
||||
<Route size={20} className="text-purple-500" />
|
||||
</button>
|
||||
)}
|
||||
{canWriteStations && (
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -225,7 +226,9 @@ export const StationListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -92,10 +92,11 @@ export const UserListPage = observer(() => {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/user/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/user/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -146,7 +147,9 @@ export const UserListPage = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
@@ -154,6 +157,11 @@ export const UserListPage = observer(() => {
|
||||
checkboxSelection={canWriteUsers}
|
||||
disableRowSelectionExcludeModel
|
||||
disableRowSelectionOnClick
|
||||
onRowDoubleClick={(params) => {
|
||||
if (canWriteUsers) {
|
||||
navigate(`/user/${params.row.id}/edit`);
|
||||
}
|
||||
}}
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
|
||||
@@ -115,15 +115,16 @@ export const VehicleListPage = observer(() => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
{canWrite && (
|
||||
<button onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
||||
<button title="Редактировать" onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
||||
<button title="Просмотр" onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
{canWrite && (
|
||||
<button
|
||||
title="Удалить"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
@@ -173,7 +174,9 @@ export const VehicleListPage = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{rows.length > 0 && (
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
)}
|
||||
|
||||
{canWriteVehicles && ids.length > 0 && (
|
||||
<div className="flex justify-end mb-5 duration-300">
|
||||
|
||||
@@ -6,11 +6,21 @@ import {
|
||||
SelectChangeEvent,
|
||||
Typography,
|
||||
Box,
|
||||
keyframes,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { authStore, cityStore, selectedCityStore, snapshotStore, type City } from "@shared";
|
||||
import { MapPin } from "lucide-react";
|
||||
|
||||
const borderSpin = keyframes`
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CitySelector: React.FC = observer(() => {
|
||||
const { selectedCity, setSelectedCity, isLocked } = selectedCityStore;
|
||||
const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities");
|
||||
@@ -43,6 +53,8 @@ export const CitySelector: React.FC = observer(() => {
|
||||
})()
|
||||
: baseCities;
|
||||
|
||||
const noCitySelected = !selectedCity?.id;
|
||||
|
||||
const handleCityChange = (event: SelectChangeEvent<string>) => {
|
||||
const cityId = event.target.value;
|
||||
if (cityId === "") {
|
||||
@@ -58,10 +70,7 @@ export const CitySelector: React.FC = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex items-center gap-2">
|
||||
<MapPin size={16} className={isLocked ? "text-gray-400" : "text-white"} />
|
||||
<FormControl size="medium" sx={{ minWidth: 120 }}>
|
||||
const selectElement = (
|
||||
<Select
|
||||
value={selectedCity?.id?.toString() || ""}
|
||||
onChange={handleCityChange}
|
||||
@@ -70,15 +79,23 @@ export const CitySelector: React.FC = observer(() => {
|
||||
sx={{
|
||||
height: "40px",
|
||||
color: "white",
|
||||
"&.Mui-disabled": {
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
WebkitTextFillColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
borderRadius: "4px",
|
||||
...(noCitySelected && !isLocked
|
||||
? {
|
||||
backgroundColor: "#48989f",
|
||||
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
||||
}
|
||||
: {
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: isLocked
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
}),
|
||||
"&.Mui-disabled": {
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
WebkitTextFillColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: isLocked
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
@@ -101,6 +118,28 @@ export const CitySelector: React.FC = observer(() => {
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="flex items-center gap-2">
|
||||
<MapPin size={16} className={isLocked ? "text-gray-400" : "text-white"} />
|
||||
<FormControl size="medium" sx={{ minWidth: 120 }}>
|
||||
{noCitySelected && !isLocked ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
borderRadius: "4px",
|
||||
padding: "2px",
|
||||
background: "linear-gradient(90deg, rgba(255,255,255,0.1), rgba(255,255,255,0.7), rgba(255,255,255,0.1), rgba(255,255,255,0.7), rgba(255,255,255,0.1))",
|
||||
backgroundSize: "200% 100%",
|
||||
animation: `${borderSpin} 2.5s linear infinite`,
|
||||
}}
|
||||
>
|
||||
{selectElement}
|
||||
</Box>
|
||||
) : (
|
||||
selectElement
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -618,7 +618,7 @@ export const DevicesTable = observer(() => {
|
||||
e.stopPropagation();
|
||||
navigate(`/vehicle/${row.vehicle_id}/edit`);
|
||||
}}
|
||||
title="Редактировать транспорт"
|
||||
title="Редактировать"
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
@@ -628,7 +628,7 @@ export const DevicesTable = observer(() => {
|
||||
e.stopPropagation();
|
||||
handleReloadStatus();
|
||||
}}
|
||||
title="Перезапросить статус"
|
||||
title="Обновить статус"
|
||||
disabled={
|
||||
!row.device_uuid || !devices.includes(row.device_uuid)
|
||||
}
|
||||
@@ -655,7 +655,7 @@ export const DevicesTable = observer(() => {
|
||||
setLogsModalOpen(true);
|
||||
}
|
||||
}}
|
||||
title="Логи устройства"
|
||||
title="Логи"
|
||||
>
|
||||
<ScrollText size={16} />
|
||||
</button>
|
||||
@@ -668,7 +668,7 @@ export const DevicesTable = observer(() => {
|
||||
setSessionsModalVehicleTailNumber(row.tail_number);
|
||||
setSessionsModalOpen(true);
|
||||
}}
|
||||
title="Сессии ТО"
|
||||
title="Сессии обслуживания"
|
||||
>
|
||||
<Wrench size={16} />
|
||||
</button>
|
||||
|
||||
@@ -87,12 +87,14 @@ export const SightsTable = observer(() => {
|
||||
<TableCell align="center" className="py-3">
|
||||
<div className="flex justify-center items-center gap-3">
|
||||
<button
|
||||
title="Редактировать"
|
||||
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
|
||||
onClick={() => navigate(`/sight/${row?.id}`)}
|
||||
>
|
||||
<Pencil size={18} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
title="Удалить"
|
||||
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
|
||||
Reference in New Issue
Block a user