feature/webgl-preview-update #21

Merged
Kerblif merged 78 commits from feature/webgl-preview-update into main 2026-10-13 16:49:19 +00:00
4 changed files with 203 additions and 5 deletions
Showing only changes of commit 42f8019e27 - Show all commits

View File

@@ -64,6 +64,7 @@ export type GetRouteResponse = {
route_sys_number: string; route_sys_number: string;
scale_max: number; scale_max: number;
scale_min: number; scale_min: number;
video_preview: string;
video_timer?: number; video_timer?: number;
icon?: string; icon?: string;
icon_size?: number; icon_size?: number;

View File

@@ -7,8 +7,12 @@ import {
InputLabel, InputLabel,
Typography, Typography,
Box, Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material"; } from "@mui/material";
import { ImageUploadCard } from "@widgets"; import { MediaViewer, VideoPreviewCard, ImageUploadCard } from "@widgets";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react"; import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
@@ -51,10 +55,14 @@ export const RouteCreatePage = observer(() => {
const [centerLat, setCenterLat] = useState(""); const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState(""); const [centerLng, setCenterLng] = useState("");
const [videoTimer, setVideoTimer] = useState(420); const [videoTimer, setVideoTimer] = useState(420);
const [videoPreview, setVideoPreview] = useState<string>("");
const [icon, setIcon] = useState<string>(""); const [icon, setIcon] = useState<string>("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false); useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
@@ -62,6 +70,7 @@ export const RouteCreatePage = observer(() => {
const [activeIconMenuType, setActiveIconMenuType] = useState< const [activeIconMenuType, setActiveIconMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null); >(null);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore; const { language } = languageStore;
useEffect(() => { useEffect(() => {
@@ -137,11 +146,42 @@ export const RouteCreatePage = observer(() => {
setIsSelectIconDialogOpen(false); setIsSelectIconDialogOpen(false);
}; };
const handleVideoSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
setVideoPreview(media.id);
setIsSelectVideoDialogOpen(false);
};
const handleVideoFileSelect = (file?: File) => {
if (file) {
setFileToUpload(file);
setIsUploadVideoDialogOpen(true);
} else {
setIsSelectVideoDialogOpen(true);
}
};
const handleVideoUpload = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
setVideoPreview(media.id);
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
};
const selectedIconMedia = const selectedIconMedia =
icon && !isMediaIdEmpty(icon) icon && !isMediaIdEmpty(icon)
? mediaStore.media.find((m) => m.id === icon) ? mediaStore.media.find((m) => m.id === icon)
: null; : null;
const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon; const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon;
const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview;
const handleCreateRoute = async () => { const handleCreateRoute = async () => {
try { try {
@@ -236,6 +276,7 @@ export const RouteCreatePage = observer(() => {
center_latitude, center_latitude,
center_longitude, center_longitude,
path, path,
video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined,
icon: !isMediaIdEmpty(icon) ? icon : undefined, icon: !isMediaIdEmpty(icon) ? icon : undefined,
video_timer: videoTimer, video_timer: videoTimer,
}; };
@@ -436,6 +477,16 @@ export const RouteCreatePage = observer(() => {
}} }}
/> />
</div> </div>
<div className="flex flex-col gap-4 max-w-[300px]">
<VideoPreviewCard
title="Видеозаставка"
videoId={effectiveVideoId}
onVideoClick={() => setIsVideoPreviewOpen(true)}
onDeleteVideoClick={() => setVideoPreview("")}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</div>
</Box> </Box>
<FormControl fullWidth required> <FormControl fullWidth required>
@@ -552,6 +603,50 @@ export const RouteCreatePage = observer(() => {
onClose={() => setIsSelectArticleDialogOpen(false)} onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect} onSelectArticle={handleArticleSelect}
/> />
<SelectMediaDialog
open={isSelectVideoDialogOpen}
onClose={() => setIsSelectVideoDialogOpen(false)}
onSelectMedia={handleVideoSelect}
mediaType={2}
/>
{effectiveVideoId && (
<Dialog
open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
<MediaViewer
media={{
id: effectiveVideoId,
media_type: 2,
filename: "video_preview",
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsVideoPreviewOpen(false)}>
Закрыть
</Button>
</DialogActions>
</Dialog>
)}
<UploadMediaDialog
open={isUploadVideoDialogOpen}
onClose={() => {
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
}}
hardcodeType="video_preview"
contextObjectName={routeName || "Маршрут"}
contextType="route"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
<SelectMediaDialog <SelectMediaDialog
open={isSelectIconDialogOpen} open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)} onClose={() => setIsSelectIconDialogOpen(false)}

View File

@@ -7,8 +7,14 @@ import {
InputLabel, InputLabel,
Typography, Typography,
Box, Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material"; } from "@mui/material";
import { import {
MediaViewer,
VideoPreviewCard,
ImageUploadCard, ImageUploadCard,
DeleteModal, DeleteModal,
} from "@widgets"; } from "@widgets";
@@ -48,6 +54,9 @@ export const RouteEditPage = observer(() => {
const [isLoadingData, setIsLoadingData] = useState(true); const [isLoadingData, setIsLoadingData] = useState(true);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false); useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
@@ -56,6 +65,7 @@ export const RouteEditPage = observer(() => {
const [activeIconMenuType, setActiveIconMenuType] = useState< const [activeIconMenuType, setActiveIconMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null); >(null);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore; const { language } = languageStore;
const [coordinates, setCoordinates] = useState<string>(""); const [coordinates, setCoordinates] = useState<string>("");
@@ -104,6 +114,9 @@ export const RouteEditPage = observer(() => {
const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon) const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon)
? null ? null
: (selectedIconMedia?.id ?? editRouteData.icon); : (selectedIconMedia?.id ?? editRouteData.icon);
const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview)
? null
: editRouteData.video_preview;
useEffect(() => { useEffect(() => {
if (editRouteData.path && editRouteData.path.length > 0) { if (editRouteData.path && editRouteData.path.length > 0) {
@@ -225,6 +238,36 @@ export const RouteEditPage = observer(() => {
articlesStore.getArticleList(); articlesStore.getArticleList();
}; };
const handleVideoSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
routeStore.setEditRouteData({ video_preview: media.id });
setIsSelectVideoDialogOpen(false);
};
const handleVideoFileSelect = (file?: File) => {
if (file) {
setFileToUpload(file);
setIsUploadVideoDialogOpen(true);
} else {
setIsSelectVideoDialogOpen(true);
}
};
const handleVideoUpload = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
routeStore.setEditRouteData({ video_preview: media.id });
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
};
// Получаем название выбранной статьи для отображения // Получаем название выбранной статьи для отображения
const selectedArticle = articlesStore.articleList.ru.data.find( const selectedArticle = articlesStore.articleList.ru.data.find(
(article) => article.id === editRouteData.governor_appeal (article) => article.id === editRouteData.governor_appeal
@@ -593,6 +636,18 @@ export const RouteEditPage = observer(() => {
}} }}
/> />
</div> </div>
<div className="flex flex-col gap-4 max-w-[300px]">
<VideoPreviewCard
title="Видеозаставка"
videoId={effectiveVideoId}
onVideoClick={() => setIsVideoPreviewOpen(true)}
onDeleteVideoClick={() =>
routeStore.setEditRouteData({ video_preview: "" })
}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</div>
</Box> </Box>
</Box> </Box>
@@ -638,6 +693,48 @@ export const RouteEditPage = observer(() => {
onClose={() => setIsSelectArticleDialogOpen(false)} onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect} onSelectArticle={handleArticleSelect}
/> />
<SelectMediaDialog
open={isSelectVideoDialogOpen}
onClose={() => setIsSelectVideoDialogOpen(false)}
onSelectMedia={handleVideoSelect}
mediaType={2}
/>
<Dialog
open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
{effectiveVideoId && (
<MediaViewer
media={{
id: effectiveVideoId,
media_type: 2,
filename: "video_preview",
}}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
</DialogActions>
</Dialog>
<UploadMediaDialog
open={isUploadVideoDialogOpen}
onClose={() => {
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
}}
hardcodeType="video_preview"
contextObjectName={editRouteData.route_name || "Маршрут"}
contextType="route"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
<SelectMediaDialog <SelectMediaDialog
open={isSelectIconDialogOpen} open={isSelectIconDialogOpen}
onClose={() => setIsSelectIconDialogOpen(false)} onClose={() => setIsSelectIconDialogOpen(false)}

View File

@@ -23,6 +23,7 @@ export type Route = {
route_sys_number: string; route_sys_number: string;
scale_max: number; scale_max: number;
scale_min: number; scale_min: number;
video_preview: string;
video_timer: number; video_timer: number;
}; };
@@ -153,16 +154,21 @@ class RouteStore {
route_sys_number: "", route_sys_number: "",
scale_max: 0, scale_max: 0,
scale_min: 0, scale_min: 0,
video_preview: "" as string | undefined,
video_timer: 420, video_timer: 420,
}; };
setEditRouteData = (data: any) => { setEditRouteData = (data: any) => {
const routeData = { ...data }; this.editRouteData = { ...this.editRouteData, ...data };
delete routeData.video_preview;
this.editRouteData = { ...this.editRouteData, ...routeData };
}; };
editRoute = async (id: number) => { editRoute = async (id: number) => {
if (
!this.editRouteData.video_preview ||
isMediaIdEmpty(this.editRouteData.video_preview)
) {
delete this.editRouteData.video_preview;
}
if (!this.editRouteData.icon || isMediaIdEmpty(this.editRouteData.icon)) { if (!this.editRouteData.icon || isMediaIdEmpty(this.editRouteData.icon)) {
delete (this.editRouteData as any).icon; delete (this.editRouteData as any).icon;
} }
@@ -174,7 +180,6 @@ class RouteStore {
center_latitude: parseFloat(this.editRouteData.center_latitude), center_latitude: parseFloat(this.editRouteData.center_latitude),
center_longitude: parseFloat(this.editRouteData.center_longitude), center_longitude: parseFloat(this.editRouteData.center_longitude),
}; };
delete dataToSend.video_preview;
if ( if (
this.editRouteData.governor_appeal === 0 || this.editRouteData.governor_appeal === 0 ||
!this.editRouteData.governor_appeal !this.editRouteData.governor_appeal