#14 Перепись редактирования и создания маршрута (#16)

Добавлено новое поле route_name:

Текстовые поля на двух страницах
Поле в списке маршрутов

Добавлено выбор видео на двух страниц вместе с редактором статей в виде модального окна

Модальное окно позволяет создать статью, выбрать готовую, отредактировать выбранную сразу на трех языках

Микаэл:

Пожалуйста, перепроверь код, вдруг чего найдешь как улучшить

+

захости локально и потыкай пж:

создай с 0 маршрут и прикрепи к нему созданную / какую-нибудь статью с видео, можешь попробовать загрузить либо взять готовое

после того как создашь, попробуй потыкать и поменять чего-нибудь

(проще обьясню: представь, что ты Руслан)

Reviewed-on: #16
Reviewed-by: Микаэл Оганесян <15lu.akari@unprism.ru>
Co-authored-by: fisenko <kkzemeow@gmail.com>
Co-committed-by: fisenko <kkzemeow@gmail.com>
This commit is contained in:
2025-10-31 11:13:08 +00:00
parent 2b48ade2f1
commit 90f3d66b22
13 changed files with 1344 additions and 271 deletions

View File

@@ -13,7 +13,7 @@ import {
DialogContent,
DialogActions,
} from "@mui/material";
import { MediaViewer } from "@widgets";
import { MediaViewer, VideoPreviewCard } from "@widgets";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
import { useEffect, useState, useMemo } from "react";
@@ -24,9 +24,10 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
import { Route, routeStore } from "../../../shared/store/RouteStore";
import {
languageStore,
SelectArticleModal,
ArticleSelectOrCreateDialog,
SelectMediaDialog,
selectedCityStore,
UploadMediaDialog,
} from "@shared";
export const RouteCreatePage = observer(() => {
@@ -39,6 +40,7 @@ export const RouteCreatePage = observer(() => {
const [direction, setDirection] = useState("backward");
const [scaleMin, setScaleMin] = useState("");
const [scaleMax, setScaleMax] = useState("");
const [routeName, setRouteName] = useState("");
const [turn, setTurn] = useState("");
const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState("");
@@ -48,6 +50,8 @@ export const RouteCreatePage = observer(() => {
useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore;
useEffect(() => {
@@ -55,7 +59,6 @@ export const RouteCreatePage = observer(() => {
articlesStore.getArticleList();
}, [language]);
// Фильтруем перевозчиков только из выбранного города
const filteredCarriers = useMemo(() => {
const carriers =
carrierStore.carriers[language as keyof typeof carrierStore.carriers]
@@ -110,6 +113,8 @@ export const RouteCreatePage = observer(() => {
const handleArticleSelect = (articleId: number) => {
setGovernorAppeal(articleId.toString());
setIsSelectArticleDialogOpen(false);
// Обновляем список статей после создания новой
articlesStore.getArticleList();
};
const handleVideoSelect = (media: {
@@ -122,6 +127,26 @@ export const RouteCreatePage = observer(() => {
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 handleVideoPreviewClick = () => {
setIsVideoPreviewOpen(true);
};
@@ -167,6 +192,7 @@ export const RouteCreatePage = observer(() => {
route_number: routeNumber,
route_sys_number: govRouteNumber,
governor_appeal,
route_name: routeName,
route_direction,
scale_min,
scale_max,
@@ -208,6 +234,13 @@ export const RouteCreatePage = observer(() => {
<div className="flex flex-col gap-10 w-full items-end">
<Box className="flex flex-col gap-6 w-full">
<TextField
className="w-full"
label="Название маршрута"
required
value={routeName}
onChange={(e) => setRouteName(e.target.value)}
/>
<FormControl fullWidth required>
<InputLabel>Выберите перевозчика</InputLabel>
<Select
@@ -247,7 +280,6 @@ export const RouteCreatePage = observer(() => {
const lines = routeCoords.split("\n");
const lastLine = lines[lines.length - 1];
// Если мы на последней строке и она не пустая
if (lastLine && lastLine.trim()) {
e.preventDefault();
const newValue = routeCoords + "\n";
@@ -279,6 +311,7 @@ export const RouteCreatePage = observer(() => {
},
}}
/>
<TextField
className="w-full"
label="Номер маршрута в Говорящем Городе"
@@ -287,99 +320,42 @@ export const RouteCreatePage = observer(() => {
onChange={(e) => setGovRouteNumber(e.target.value)}
/>
{/* Заменяем Select на кнопку для выбора статьи */}
<Box className="flex flex-col gap-2">
<label className="text-sm font-medium text-gray-700">
Обращение к пассажирам
</label>
<Box className="flex gap-2">
<TextField
className="flex-1"
value={selectedArticle?.heading || "Статья не выбрана"}
placeholder="Выберите статью"
disabled
sx={{
"& .MuiInputBase-input": {
color: selectedArticle ? "inherit" : "#999",
},
}}
/>
<Button
variant="outlined"
onClick={() => setIsSelectArticleDialogOpen(true)}
startIcon={<Plus size={16} />}
sx={{ minWidth: "auto", px: 2 }}
>
Выбрать
</Button>
</Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Обращение к пассажирам
</Typography>
<Box className="flex gap-2">
<TextField
className="flex-1"
value={selectedArticle?.heading || "Статья не выбрана"}
placeholder="Выберите статью"
disabled
fullWidth
sx={{
"& .MuiInputBase-input": {
color: selectedArticle ? "inherit" : "#999",
},
}}
/>
<Button
variant="outlined"
onClick={() => setIsSelectArticleDialogOpen(true)}
startIcon={<Plus size={16} />}
sx={{ minWidth: "auto", px: 2 }}
>
Выбрать
</Button>
</Box>
{/* Селектор видеозаставки */}
<Box className="flex flex-col gap-2">
<label className="text-sm font-medium text-gray-700">
Видеозаставка
</label>
<Box className="flex gap-2">
<Box
className="flex-1"
onClick={handleVideoPreviewClick}
sx={{
cursor:
videoPreview && videoPreview !== "" ? "pointer" : "default",
}}
>
<Box
className="w-full h-[50px] border border-gray-400 rounded-sm flex items-center justify-between px-4"
sx={{
"& .MuiInputBase-input": {
color:
videoPreview && videoPreview !== ""
? "inherit"
: "#999",
cursor:
videoPreview && videoPreview !== ""
? "pointer"
: "default",
},
}}
>
<Typography variant="body1" className="text-sm">
{videoPreview && videoPreview !== ""
? "Видео выбрано"
: "Видео не выбрано"}
</Typography>
{videoPreview && videoPreview !== "" && (
<Box
onClick={(e) => {
e.stopPropagation();
setVideoPreview("");
}}
sx={{
cursor: "pointer",
color: "#999",
"&:hover": {
color: "#666",
},
}}
>
<Typography variant="body1" className="text-lg font-bold">
×
</Typography>
</Box>
)}
</Box>
</Box>
<Button
variant="outlined"
onClick={() => setIsSelectVideoDialogOpen(true)}
startIcon={<Plus size={16} />}
sx={{ minWidth: "auto", px: 2 }}
>
Выбрать
</Button>
</Box>
</Box>
<VideoPreviewCard
title="Видеозаставка"
videoId={videoPreview}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
setVideoPreview("");
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
<FormControl fullWidth required>
<InputLabel>Прямой/обратный маршрут</InputLabel>
@@ -404,6 +380,7 @@ export const RouteCreatePage = observer(() => {
value={scaleMax}
onChange={(e) => setScaleMax(e.target.value)}
/>
<TextField
className="w-full"
label="Поворот"
@@ -440,23 +417,17 @@ export const RouteCreatePage = observer(() => {
</Button>
</div>
</div>
{/* Модальное окно выбора статьи */}
<SelectArticleModal
<ArticleSelectOrCreateDialog
open={isSelectArticleDialogOpen}
onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect}
/>
{/* Модальное окно выбора видео */}
<SelectMediaDialog
open={isSelectVideoDialogOpen}
onClose={() => setIsSelectVideoDialogOpen(false)}
onSelectMedia={handleVideoSelect}
mediaType={2}
/>
{/* Модальное окно предпросмотра видео */}
{videoPreview && videoPreview !== "" && (
<Dialog
open={isVideoPreviewOpen}
@@ -483,6 +454,18 @@ export const RouteCreatePage = observer(() => {
</DialogActions>
</Dialog>
)}
<UploadMediaDialog
open={isUploadVideoDialogOpen}
onClose={() => {
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
}}
hardcodeType="video_preview"
contextObjectName={routeName || "Маршрут"}
contextType="sight"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
</Paper>
);
});