feat: Rewrite route edit and create page for new field

This commit is contained in:
2025-10-30 01:26:32 +03:00
parent 2b48ade2f1
commit 3526648d1f
8 changed files with 1310 additions and 213 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(() => {
@@ -110,6 +114,8 @@ export const RouteCreatePage = observer(() => {
const handleArticleSelect = (articleId: number) => {
setGovernorAppeal(articleId.toString());
setIsSelectArticleDialogOpen(false);
// Обновляем список статей после создания новой
articlesStore.getArticleList();
};
const handleVideoSelect = (media: {
@@ -122,6 +128,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 +193,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 +235,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
@@ -279,6 +313,7 @@ export const RouteCreatePage = observer(() => {
},
}}
/>
<TextField
className="w-full"
label="Номер маршрута в Говорящем Городе"
@@ -287,99 +322,43 @@ 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 +383,7 @@ export const RouteCreatePage = observer(() => {
value={scaleMax}
onChange={(e) => setScaleMax(e.target.value)}
/>
<TextField
className="w-full"
label="Поворот"
@@ -441,8 +421,8 @@ export const RouteCreatePage = observer(() => {
</div>
</div>
{/* Модальное окно выбора статьи */}
<SelectArticleModal
{/* Модальное окно выбора или создания статьи */}
<ArticleSelectOrCreateDialog
open={isSelectArticleDialogOpen}
onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect}
@@ -483,6 +463,20 @@ 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>
);
});

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, Copy, Save, Plus } from "lucide-react";
import { useEffect, useState } from "react";
@@ -24,8 +24,9 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
import {
routeStore,
languageStore,
SelectArticleModal,
ArticleSelectOrCreateDialog,
SelectMediaDialog,
UploadMediaDialog,
} from "@shared";
import { toast } from "react-toastify";
import { stationsStore } from "@shared";
@@ -40,6 +41,8 @@ export const RouteEditPage = 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;
const [coordinates, setCoordinates] = useState<string>("");
@@ -125,6 +128,8 @@ export const RouteEditPage = observer(() => {
governor_appeal: articleId,
});
setIsSelectArticleDialogOpen(false);
// Обновляем список статей после создания новой
articlesStore.getArticleList();
};
const handleVideoSelect = (media: {
@@ -139,6 +144,28 @@ export const RouteEditPage = 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;
}) => {
routeStore.setEditRouteData({
video_preview: media.id,
});
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
};
const handleVideoPreviewClick = () => {
if (editRouteData.video_preview && editRouteData.video_preview !== "") {
setIsVideoPreviewOpen(true);
@@ -164,6 +191,17 @@ export const RouteEditPage = 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={editRouteData.route_name || ""}
onChange={(e) =>
routeStore.setEditRouteData({
route_name: e.target.value,
})
}
/>
<FormControl fullWidth required>
<InputLabel>Выберите перевозчика</InputLabel>
<Select
@@ -279,110 +317,6 @@ export const RouteEditPage = observer(() => {
}
/>
{/* Заменяем 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>
</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:
editRouteData.video_preview &&
editRouteData.video_preview !== ""
? "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:
editRouteData.video_preview &&
editRouteData.video_preview !== ""
? "inherit"
: "#999",
cursor:
editRouteData.video_preview &&
editRouteData.video_preview !== ""
? "pointer"
: "default",
},
}}
>
<Typography variant="body1" className="text-sm">
{editRouteData.video_preview &&
editRouteData.video_preview !== ""
? "Видео выбрано"
: "Видео не выбрано"}
</Typography>
{editRouteData.video_preview &&
editRouteData.video_preview !== "" && (
<Box
onClick={(e) => {
e.stopPropagation();
routeStore.setEditRouteData({ video_preview: "" });
}}
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>
<FormControl fullWidth required>
<InputLabel>Прямой/обратный маршрут</InputLabel>
<Select
@@ -453,6 +387,45 @@ export const RouteEditPage = observer(() => {
})
}
/>
<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>
{/* Правая часть - Видео (30%) */}
<VideoPreviewCard
title="Видеозаставка"
videoId={editRouteData.video_preview}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
routeStore.setEditRouteData({ video_preview: "" });
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</Box>
<LinkedItems
@@ -494,8 +467,8 @@ export const RouteEditPage = observer(() => {
</div>
</div>
{/* Модальное окно выбора статьи */}
<SelectArticleModal
{/* Модальное окно выбора или создания статьи */}
<ArticleSelectOrCreateDialog
open={isSelectArticleDialogOpen}
onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect}
@@ -519,19 +492,35 @@ export const RouteEditPage = observer(() => {
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
<MediaViewer
media={{
id: editRouteData.video_preview,
media_type: 2,
filename: "video_preview",
}}
/>
{editRouteData.video_preview && (
<MediaViewer
media={{
id: editRouteData.video_preview,
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="sight"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
</Paper>
);
});

View File

@@ -50,6 +50,22 @@ export const RouteListPage = observer(() => {
);
},
},
{
field: "route_name",
headerName: "Название маршрута",
flex: 1,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="w-full h-full flex items-center">
{params.value ? (
params.value
) : (
<Minus size={20} className="text-red-500" />
)}
</div>
);
},
},
{
field: "route_number",
headerName: "Номер маршрута",
@@ -122,6 +138,7 @@ export const RouteListPage = observer(() => {
carrier_id: route.carrier_id,
route_number: route.route_number,
route_direction: route.route_direction ? "Прямой" : "Обратный",
route_name: route.route_name,
}));
return (

File diff suppressed because it is too large Load Diff

View File

@@ -2,3 +2,4 @@ export * from "./SelectArticleDialog";
export * from "./SelectMediaDialog";
export * from "./PreviewMediaDialog";
export * from "./UploadMediaDialog";
export * from "./ArticleSelectOrCreateDialog";

View File

@@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx";
import { authInstance } from "@shared";
export type Route = {
route_name: string;
carrier: string;
carrier_id: number;
center_latitude: number;
@@ -97,6 +98,7 @@ class RouteStore {
};
editRouteData = {
route_name: "",
carrier: "",
carrier_id: 0,
center_latitude: "",
@@ -110,7 +112,7 @@ class RouteStore {
route_sys_number: "",
scale_max: 0,
scale_min: 0,
video_preview: "",
video_preview: "" as string | undefined,
};
setEditRouteData = (data: any) => {
@@ -118,6 +120,9 @@ class RouteStore {
};
editRoute = async (id: number) => {
if (!this.editRouteData.video_preview) {
delete this.editRouteData.video_preview;
}
const response = await authInstance.patch(`/route/${id}`, {
...this.editRouteData,
center_latitude: parseFloat(this.editRouteData.center_latitude),

View File

@@ -11,6 +11,7 @@ interface VideoPreviewCardProps {
onDeleteVideoClick: () => void;
onSelectVideoClick: (file?: File) => void;
tooltipText?: string;
className?: string;
}
export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
@@ -20,6 +21,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
onDeleteVideoClick,
onSelectVideoClick,
tooltipText,
className,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragOver, setIsDragOver] = useState(false);
@@ -89,7 +91,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
gap: 1,
flex: 1,
minWidth: 150,
width: "min-content",
mx: "auto",
}}
className={className}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
@@ -127,7 +132,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
</button>
)}
{videoId ? (
<Box sx={{ position: "relative", width: "100%", height: "100%" }}>
<Box
sx={{ position: "relative", width: "100%", height: "100%" }}
className={className}
>
<video
src={`${
import.meta.env.VITE_KRBL_MEDIA

File diff suppressed because one or more lines are too long