feat: big major update

This commit is contained in:
2026-02-02 04:00:37 +03:00
parent bbab6fc46a
commit d557664b25
34 changed files with 1801 additions and 665 deletions

View File

@@ -13,22 +13,30 @@ import {
DialogContent,
DialogActions,
} from "@mui/material";
import { MediaViewer, VideoPreviewCard } from "@widgets";
import {
MediaViewer,
VideoPreviewCard,
ImageUploadCard,
} from "@widgets";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react";
import { useEffect, useState, useMemo } 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";
import {
carrierStore,
articlesStore,
routeStore,
languageStore,
mediaStore,
isMediaIdEmpty,
ArticleSelectOrCreateDialog,
SelectMediaDialog,
selectedCityStore,
UploadMediaDialog,
PreviewMediaDialog,
} from "@shared";
import type { Route } from "@shared";
export const RouteCreatePage = observer(() => {
const navigate = useNavigate();
@@ -45,18 +53,27 @@ export const RouteCreatePage = observer(() => {
const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState("");
const [videoPreview, setVideoPreview] = useState<string>("");
const [icon, setIcon] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
const [previewIconId, setPreviewIconId] = useState("");
const [activeIconMenuType, setActiveIconMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore;
useEffect(() => {
carrierStore.getCarriers(language);
articlesStore.getArticleList();
mediaStore.getMedia();
}, [language]);
const filteredCarriers = useMemo(() => {
@@ -150,6 +167,23 @@ export const RouteCreatePage = observer(() => {
setIsVideoPreviewOpen(true);
};
const handleIconSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
setIcon(media.id);
setIsSelectIconDialogOpen(false);
};
const selectedIconMedia =
icon && !isMediaIdEmpty(icon)
? mediaStore.media.find((m) => m.id === icon)
: null;
const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon;
const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview;
const handleCreateRoute = async () => {
try {
setIsLoading(true);
@@ -243,8 +277,8 @@ export const RouteCreatePage = observer(() => {
center_latitude,
center_longitude,
path,
video_preview:
videoPreview && videoPreview !== "" ? videoPreview : undefined,
video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined,
icon: !isMediaIdEmpty(icon) ? icon : undefined,
};
if (governor_appeal !== undefined) {
@@ -403,16 +437,41 @@ export const RouteCreatePage = observer(() => {
</Button>
</Box>
<VideoPreviewCard
title="Видеозаставка"
videoId={videoPreview}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
setVideoPreview("");
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
<Box className="w-full flex justify-center gap-4 flex-wrap">
<div className="flex flex-col gap-4 max-w-[300px]">
<ImageUploadCard
title="Иконка маршрута"
imageKey="thumbnail"
imageUrl={effectiveIconUrl}
onImageClick={() => {
setIsPreviewIconOpen(true);
setPreviewIconId(selectedIconMedia?.id ?? icon ?? "");
}}
onDeleteImageClick={() => {
setIcon("");
setActiveIconMenuType(null);
}}
onSelectFileClick={() => {
setActiveIconMenuType("image");
setIsSelectIconDialogOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadIconDialogOpen(true);
setActiveIconMenuType("image");
}}
/>
</div>
<div className="flex flex-col gap-4 max-w-[300px]">
<VideoPreviewCard
title="Видеозаставка"
videoId={effectiveVideoId}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => setVideoPreview("")}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</div>
</Box>
<FormControl fullWidth required>
<InputLabel>Прямой/обратный маршрут</InputLabel>
@@ -522,7 +581,7 @@ export const RouteCreatePage = observer(() => {
onSelectMedia={handleVideoSelect}
mediaType={2}
/>
{videoPreview && videoPreview !== "" && (
{effectiveVideoId && (
<Dialog
open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)}
@@ -534,7 +593,7 @@ export const RouteCreatePage = observer(() => {
<Box className="flex justify-center items-center p-4">
<MediaViewer
media={{
id: videoPreview,
id: effectiveVideoId,
media_type: 2,
filename: "video_preview",
}}
@@ -560,6 +619,28 @@ export const RouteCreatePage = observer(() => {
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
<SelectMediaDialog
open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)}
onSelectMedia={handleIconSelect}
mediaType={1}
/>
<UploadMediaDialog
open={isUploadIconDialogOpen}
onClose={() => setIsUploadIconDialogOpen(false)}
contextObjectName={routeName || "Маршрут"}
contextType="route"
afterUpload={handleIconSelect}
hardcodeType={activeIconMenuType}
/>
<PreviewMediaDialog
open={isPreviewIconOpen}
onClose={() => setIsPreviewIconOpen(false)}
mediaId={previewIconId}
/>
</Paper>
);
});

View File

@@ -13,24 +13,31 @@ import {
DialogContent,
DialogActions,
} from "@mui/material";
import { MediaViewer, VideoPreviewCard } from "@widgets";
import {
MediaViewer,
VideoPreviewCard,
ImageUploadCard,
DeleteModal,
} from "@widgets";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Copy, Save, Plus, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { carrierStore } from "../../../shared/store/CarrierStore";
import { articlesStore } from "../../../shared/store/ArticlesStore";
import { toast } from "react-toastify";
import {
carrierStore,
articlesStore,
routeStore,
languageStore,
mediaStore,
isMediaIdEmpty,
stationsStore,
ArticleSelectOrCreateDialog,
SelectMediaDialog,
UploadMediaDialog,
PreviewMediaDialog,
LoadingSpinner,
} from "@shared";
import { toast } from "react-toastify";
import { stationsStore } from "@shared";
import { LinkedItems } from "../LinekedStations";
export const RouteEditPage = observer(() => {
@@ -44,6 +51,14 @@ export const RouteEditPage = observer(() => {
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false);
const [previewIconId, setPreviewIconId] = useState("");
const [activeIconMenuType, setActiveIconMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore;
const [coordinates, setCoordinates] = useState<string>("");
@@ -71,10 +86,32 @@ export const RouteEditPage = observer(() => {
await carrierStore.getCarriers(language);
await stationsStore.getStations();
await articlesStore.getArticleList();
await mediaStore.getMedia();
};
fetchData();
}, [id, language]);
const handleIconSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
routeStore.setEditRouteData({ icon: media.id });
setIsSelectIconDialogOpen(false);
};
const selectedIconMedia =
editRouteData.icon && !isMediaIdEmpty(editRouteData.icon)
? mediaStore.media.find((m) => m.id === editRouteData.icon)
: null;
const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon)
? null
: (selectedIconMedia?.id ?? editRouteData.icon);
const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview)
? null
: editRouteData.video_preview;
useEffect(() => {
if (editRouteData.path && editRouteData.path.length > 0) {
const formattedPath = editRouteData.path
@@ -552,16 +589,42 @@ export const RouteEditPage = observer(() => {
</Button>
</Box>
<VideoPreviewCard
title="Видеозаставка"
videoId={editRouteData.video_preview}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
routeStore.setEditRouteData({ video_preview: "" });
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
<Box className="w-full flex justify-center gap-4 flex-wrap">
<div className="flex flex-col gap-4 max-w-[300px]">
<ImageUploadCard
title="Иконка маршрута"
imageKey="thumbnail"
imageUrl={effectiveIconUrl}
onImageClick={() => {
setIsPreviewIconOpen(true);
setPreviewIconId(
selectedIconMedia?.id ?? editRouteData.icon ?? ""
);
}}
onDeleteImageClick={() => setIsDeleteIconModalOpen(true)}
onSelectFileClick={() => {
setActiveIconMenuType("image");
setIsSelectIconDialogOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadIconDialogOpen(true);
setActiveIconMenuType("image");
}}
/>
</div>
<div className="flex flex-col gap-4 max-w-[300px]">
<VideoPreviewCard
title="Видеозаставка"
videoId={effectiveVideoId}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
routeStore.setEditRouteData({ video_preview: "" });
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</div>
</Box>
</Box>
<LinkedItems
@@ -621,10 +684,10 @@ export const RouteEditPage = observer(() => {
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
{editRouteData.video_preview && (
{effectiveVideoId && (
<MediaViewer
media={{
id: editRouteData.video_preview,
id: effectiveVideoId,
media_type: 2,
filename: "video_preview",
}}
@@ -648,6 +711,38 @@ export const RouteEditPage = observer(() => {
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
<SelectMediaDialog
open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)}
onSelectMedia={handleIconSelect}
mediaType={1}
/>
<UploadMediaDialog
open={isUploadIconDialogOpen}
onClose={() => setIsUploadIconDialogOpen(false)}
contextObjectName={editRouteData.route_name || "Маршрут"}
contextType="route"
afterUpload={handleIconSelect}
hardcodeType={activeIconMenuType}
/>
<PreviewMediaDialog
open={isPreviewIconOpen}
onClose={() => setIsPreviewIconOpen(false)}
mediaId={previewIconId}
/>
<DeleteModal
open={isDeleteIconModalOpen}
onDelete={() => {
routeStore.setEditRouteData({ icon: "" });
setIsDeleteIconModalOpen(false);
}}
onCancel={() => setIsDeleteIconModalOpen(false)}
edit
/>
</Paper>
);
});

View File

@@ -4,7 +4,7 @@ import { MediaViewer } from "@widgets";
import { useMapData } from "./MapDataContext";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { authInstance } from "@shared";
import { authInstance, isMediaIdEmpty } from "@shared";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import LanguageSelector from "./web-gl/LanguageSelector";
@@ -106,7 +106,7 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
gap: 10,
}}
>
{carrierThumbnail && (
{carrierThumbnail && !isMediaIdEmpty(carrierThumbnail) && (
<MediaViewer
media={{
id: carrierThumbnail,
@@ -143,7 +143,7 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
justifyContent="center"
flexGrow={1}
>
{carrierLogo && (
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
<MediaViewer
media={{
id: carrierLogo,

View File

@@ -33,6 +33,7 @@ export interface StationData {
address: string;
city_id?: number;
description: string;
icon?: string;
id: number;
latitude: number;
longitude: number;

View File

@@ -1960,6 +1960,11 @@ export const WebGLRouteMapPrototype = observer(() => {
? { right: 0, transform: "none" }
: { left: "50%", transform: "translateX(-50%)" };
const iconUrl = station.icon
? `${import.meta.env.VITE_KRBL_MEDIA}${station.icon}/download?token=${localStorage.getItem("token") ?? ""}`
: null;
const iconSizePx = Math.round(primaryFontSize * 1.2);
const secondaryLineHeight = 1.2;
const secondaryHeight = showSecondary
? secondaryFontSize * secondaryLineHeight
@@ -2019,10 +2024,24 @@ export const WebGLRouteMapPrototype = observer(() => {
<div
style={{
position: "relative",
display: "inline-block",
display: "inline-flex",
alignItems: "center",
gap: iconUrl ? 6 : 0,
pointerEvents: "none",
}}
>
{iconUrl ? (
<img
src={iconUrl}
alt=""
style={{
width: iconSizePx,
height: iconSizePx,
flexShrink: 0,
pointerEvents: "none",
}}
/>
) : null}
<div
style={{
fontWeight: 700,
@@ -2263,4 +2282,4 @@ export const WebGLRouteMapPrototype = observer(() => {
);
});
export default WebGLRouteMapPrototype;
export default WebGLRouteMapPrototype;