3 Commits
develop ... #14

13 changed files with 1344 additions and 271 deletions

View File

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

View File

@@ -13,7 +13,7 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
} from "@mui/material"; } from "@mui/material";
import { MediaViewer } from "@widgets"; import { MediaViewer, VideoPreviewCard } from "@widgets";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Copy, Save, Plus } from "lucide-react"; import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -24,8 +24,9 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
import { import {
routeStore, routeStore,
languageStore, languageStore,
SelectArticleModal, ArticleSelectOrCreateDialog,
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog,
} from "@shared"; } from "@shared";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { stationsStore } from "@shared"; import { stationsStore } from "@shared";
@@ -40,12 +41,13 @@ export const RouteEditPage = observer(() => {
useState(false); useState(false);
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false); const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const { language } = languageStore; const { language } = languageStore;
const [coordinates, setCoordinates] = useState<string>(""); const [coordinates, setCoordinates] = useState<string>("");
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
// Устанавливаем русский язык при загрузке страницы
const response = await routeStore.getRoute(Number(id)); const response = await routeStore.getRoute(Number(id));
routeStore.setEditRouteData(response); routeStore.setEditRouteData(response);
languageStore.setLanguage("ru"); languageStore.setLanguage("ru");
@@ -125,6 +127,8 @@ export const RouteEditPage = observer(() => {
governor_appeal: articleId, governor_appeal: articleId,
}); });
setIsSelectArticleDialogOpen(false); setIsSelectArticleDialogOpen(false);
// Обновляем список статей после создания новой
articlesStore.getArticleList();
}; };
const handleVideoSelect = (media: { const handleVideoSelect = (media: {
@@ -139,6 +143,28 @@ export const RouteEditPage = observer(() => {
setIsSelectVideoDialogOpen(false); 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 = () => { const handleVideoPreviewClick = () => {
if (editRouteData.video_preview && editRouteData.video_preview !== "") { if (editRouteData.video_preview && editRouteData.video_preview !== "") {
setIsVideoPreviewOpen(true); setIsVideoPreviewOpen(true);
@@ -164,6 +190,17 @@ export const RouteEditPage = observer(() => {
<div className="flex flex-col gap-10 w-full items-end"> <div className="flex flex-col gap-10 w-full items-end">
<Box className="flex flex-col gap-6 w-full"> <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> <FormControl fullWidth required>
<InputLabel>Выберите перевозчика</InputLabel> <InputLabel>Выберите перевозчика</InputLabel>
<Select <Select
@@ -235,7 +272,6 @@ export const RouteEditPage = observer(() => {
const lines = coordinates.split("\n"); const lines = coordinates.split("\n");
const lastLine = lines[lines.length - 1]; const lastLine = lines[lines.length - 1];
// Если мы на последней строке и она не пустая
if (lastLine && lastLine.trim()) { if (lastLine && lastLine.trim()) {
e.preventDefault(); e.preventDefault();
const newValue = coordinates + "\n"; const newValue = coordinates + "\n";
@@ -279,110 +315,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> <FormControl fullWidth required>
<InputLabel>Прямой/обратный маршрут</InputLabel> <InputLabel>Прямой/обратный маршрут</InputLabel>
<Select <Select
@@ -453,6 +385,43 @@ 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>
<VideoPreviewCard
title="Видеозаставка"
videoId={editRouteData.video_preview}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
routeStore.setEditRouteData({ video_preview: "" });
}}
onSelectVideoClick={handleVideoFileSelect}
className="w-full"
/>
</Box> </Box>
<LinkedItems <LinkedItems
@@ -493,23 +462,17 @@ export const RouteEditPage = observer(() => {
</Button> </Button>
</div> </div>
</div> </div>
<ArticleSelectOrCreateDialog
{/* Модальное окно выбора статьи */}
<SelectArticleModal
open={isSelectArticleDialogOpen} open={isSelectArticleDialogOpen}
onClose={() => setIsSelectArticleDialogOpen(false)} onClose={() => setIsSelectArticleDialogOpen(false)}
onSelectArticle={handleArticleSelect} onSelectArticle={handleArticleSelect}
/> />
{/* Модальное окно выбора видео */}
<SelectMediaDialog <SelectMediaDialog
open={isSelectVideoDialogOpen} open={isSelectVideoDialogOpen}
onClose={() => setIsSelectVideoDialogOpen(false)} onClose={() => setIsSelectVideoDialogOpen(false)}
onSelectMedia={handleVideoSelect} onSelectMedia={handleVideoSelect}
mediaType={2} mediaType={2}
/> />
{/* Модальное окно предпросмотра видео */}
<Dialog <Dialog
open={isVideoPreviewOpen} open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)} onClose={() => setIsVideoPreviewOpen(false)}
@@ -519,19 +482,33 @@ export const RouteEditPage = observer(() => {
<DialogTitle>Предпросмотр видео</DialogTitle> <DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent> <DialogContent>
<Box className="flex justify-center items-center p-4"> <Box className="flex justify-center items-center p-4">
<MediaViewer {editRouteData.video_preview && (
media={{ <MediaViewer
id: editRouteData.video_preview, media={{
media_type: 2, id: editRouteData.video_preview,
filename: "video_preview", media_type: 2,
}} filename: "video_preview",
/> }}
/>
)}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button> <Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<UploadMediaDialog
open={isUploadVideoDialogOpen}
onClose={() => {
setIsUploadVideoDialogOpen(false);
setFileToUpload(null);
}}
hardcodeType="video_preview"
contextObjectName={editRouteData.route_name || "Маршрут"}
contextType="sight"
initialFile={fileToUpload || undefined}
afterUpload={handleVideoUpload}
/>
</Paper> </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", field: "route_number",
headerName: "Номер маршрута", headerName: "Номер маршрута",
@@ -100,9 +116,7 @@ export const RouteListPage = observer(() => {
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}> <button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
<Map size={20} className="text-purple-500" /> <Map size={20} className="text-purple-500" />
</button> </button>
{/* <button onClick={() => navigate(`/route/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
</button> */}
<button <button
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@@ -122,6 +136,7 @@ export const RouteListPage = observer(() => {
carrier_id: route.carrier_id, carrier_id: route.carrier_id,
route_number: route.route_number, route_number: route.route_number,
route_direction: route.route_direction ? "Прямой" : "Обратный", route_direction: route.route_direction ? "Прямой" : "Обратный",
route_name: route.route_name,
})); }));
return ( return (

View File

@@ -71,10 +71,8 @@ export const clearBlobAndGLTFCache = async (url: string) => {
*/ */
export const clearMediaTransitionCache = async ( export const clearMediaTransitionCache = async (
previousMediaId: string | number | null, previousMediaId: string | number | null,
newMediaId: string | number | null,
newMediaType?: number newMediaType?: number
) => { ) => {
console.log(newMediaId, newMediaType);
// Если переключаемся с/на 3D модель, очищаем весь кеш // Если переключаемся с/на 3D модель, очищаем весь кеш
if (newMediaType === 6 || previousMediaId) { if (newMediaType === 6 || previousMediaId) {
await clearAllGLTFCache(); await clearAllGLTFCache();

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -340,55 +340,63 @@ class CreateSightStore {
createLeftArticle = async () => { createLeftArticle = async () => {
/* ... your existing logic to create a new left article (placeholder or DB) ... */ /* ... your existing logic to create a new left article (placeholder or DB) ... */
const ruName = (this.sight.ru.name || "").trim();
const enName = (this.sight.en.name || "").trim();
const zhName = (this.sight.zh.name || "").trim();
// If all names are empty, skip defaulting and use empty headings
const hasAnyName = !!(ruName || enName || zhName);
const response = await languageInstance("ru").post("/article", { const response = await languageInstance("ru").post("/article", {
heading: "Новая левая статья", heading: hasAnyName ? ruName : "",
body: "Заполните контентом", body: "",
}); });
const newLeftArticleId = response.data.id; const newLeftArticleId = response.data.id;
await languageInstance("en").patch(`/article/${newLeftArticleId}`, { await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
heading: "New Left Article", heading: hasAnyName ? enName : "",
body: "Fill with content", body: "",
}); });
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, { await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
heading: "新的左侧文章", heading: hasAnyName ? zhName : "",
body: "填写内容", body: "",
}); });
runInAction(() => { runInAction(() => {
this.sight.left_article = newLeftArticleId; // Store the actual ID this.sight.left_article = newLeftArticleId; // Store the actual ID
this.sight.ru.left = { this.sight.ru.left = {
heading: "Новая левая статья", heading: hasAnyName ? ruName : "",
body: "Заполните контентом", body: "",
media: [], media: [],
}; };
this.sight.en.left = { this.sight.en.left = {
heading: "New Left Article", heading: hasAnyName ? enName : "",
body: "Fill with content", body: "",
media: [], media: [],
}; };
this.sight.zh.left = { this.sight.zh.left = {
heading: "新的左侧文章", heading: hasAnyName ? zhName : "",
body: "填写内容", body: "",
media: [], media: [],
}; };
articlesStore.articles.ru.push({ articlesStore.articles.ru.push({
id: newLeftArticleId, id: newLeftArticleId,
heading: "Новая левая статья", heading: hasAnyName ? ruName : "",
body: "Заполните контентом", body: "",
service_name: "Новая левая статья", service_name: hasAnyName ? ruName : "",
}); });
articlesStore.articles.en.push({ articlesStore.articles.en.push({
id: newLeftArticleId, id: newLeftArticleId,
heading: "New Left Article", heading: hasAnyName ? enName : "",
body: "Fill with content", body: "",
service_name: "New Left Article", service_name: hasAnyName ? enName : "",
}); });
articlesStore.articles.zh.push({ articlesStore.articles.zh.push({
id: newLeftArticleId, id: newLeftArticleId,
heading: "新的左侧文章", heading: hasAnyName ? zhName : "",
body: "填写内容", body: "",
service_name: "新的左侧文章", service_name: hasAnyName ? zhName : "",
}); });
}); });
return newLeftArticleId; return newLeftArticleId;

View File

@@ -400,16 +400,36 @@ class EditSightStore {
}; };
createLeftArticle = async () => { createLeftArticle = async () => {
const ruName = (this.sight.ru.name || "").trim();
const enName = (this.sight.en.name || "").trim();
const zhName = (this.sight.zh.name || "").trim();
const hasAnyName = !!(ruName || enName || zhName);
const response = await languageInstance("ru").post(`/article`, { const response = await languageInstance("ru").post(`/article`, {
heading: "", heading: hasAnyName ? ruName : "",
body: "", body: "",
}); });
this.sight.common.left_article = response.data.id; this.sight.common.left_article = response.data.id;
this.sight.ru.left.heading = ""; await languageInstance("en").patch(
this.sight.en.left.heading = ""; `/article/${this.sight.common.left_article}`,
this.sight.zh.left.heading = ""; {
heading: hasAnyName ? enName : "",
body: "",
}
);
await languageInstance("zh").patch(
`/article/${this.sight.common.left_article}`,
{
heading: hasAnyName ? zhName : "",
body: "",
}
);
this.sight.ru.left.heading = hasAnyName ? ruName : "";
this.sight.en.left.heading = hasAnyName ? enName : "";
this.sight.zh.left.heading = hasAnyName ? zhName : "";
this.sight.ru.left.body = ""; this.sight.ru.left.body = "";
this.sight.en.left.body = ""; this.sight.en.left.body = "";
this.sight.zh.left.body = ""; this.sight.zh.left.body = "";

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useRef, useState, DragEvent, useEffect } from "react"; import React, { useRef, DragEvent } from "react";
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material"; import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
import { editSightStore } from "@shared"; import { editSightStore } from "@shared";
@@ -27,18 +27,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
tooltipText, tooltipText,
}) => { }) => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragOver, setIsDragOver] = useState(false);
const { setFileToUpload } = editSightStore; const { setFileToUpload } = editSightStore;
useEffect(() => {
if (isDragOver) {
console.log("isDragOver");
}
}, [isDragOver]);
// --- Click to select file ---
const handleZoneClick = () => { const handleZoneClick = () => {
// Trigger the hidden file input click
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
@@ -68,19 +59,16 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
const handleDragOver = (event: DragEvent<HTMLDivElement>) => { const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Crucial to allow a drop event.preventDefault(); // Crucial to allow a drop
event.stopPropagation(); event.stopPropagation();
setIsDragOver(true);
}; };
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => { const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setIsDragOver(false);
}; };
const handleDrop = async (event: DragEvent<HTMLDivElement>) => { const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Crucial to allow a drop event.preventDefault(); // Crucial to allow a drop
event.stopPropagation(); event.stopPropagation();
setIsDragOver(false);
const files = event.dataTransfer.files; const files = event.dataTransfer.files;
if (files && files.length > 0) { if (files && files.length > 0) {

View File

@@ -38,7 +38,7 @@ export function MediaViewer({
// Используем новый cache manager для очистки кеша // Используем новый cache manager для очистки кеша
clearMediaTransitionCache( clearMediaTransitionCache(
previousMediaId, previousMediaId,
media?.id || null,
media?.media_type media?.media_type
); );

View File

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

File diff suppressed because one or more lines are too long