feature/webgl-preview-update (#21)

Co-authored-by: itoshi <kkzemeow@gmail.com>
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-10-13 16:49:18 +00:00
parent 50ad374cf5
commit 71834904a1
327 changed files with 39034 additions and 8829 deletions

View File

@@ -1,29 +1,44 @@
import {
Button,
Paper,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import {
stationsStore,
languageStore,
cityStore,
authStore,
mediaStore,
isMediaIdEmpty,
useSelectedCity,
selectedCityStore,
SelectMediaDialog,
UploadMediaDialog,
PreviewMediaDialog,
} from "@shared";
import { useEffect, useState } from "react";
import { LanguageSwitcher } from "@widgets";
import { SaveWithoutCityAgree } from "@widgets";
import {
ImageUploadCard,
LanguageSwitcher,
SaveWithoutCityAgree,
} from "@widgets";
export const StationCreatePage = observer(() => {
const navigate = useNavigate();
useEffect(() => {
selectedCityStore.setIsLocked(true);
return () => selectedCityStore.setIsLocked(false);
}, []);
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
const {
@@ -32,10 +47,18 @@ export const StationCreatePage = observer(() => {
createStation,
setLanguageCreateStationData,
} = stationsStore;
const { cities, getCities } = cityStore;
const { getCities } = cityStore;
const canReadCities = authStore.canRead("cities");
const { selectedCityId, selectedCity } = useSelectedCity();
const [coordinates, setCoordinates] = useState<string>("");
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
useEffect(() => {
@@ -49,26 +72,27 @@ export const StationCreatePage = observer(() => {
}
}, [createStationData.common.latitude, createStationData.common.longitude]);
// НОВАЯ ФУНКЦИЯ: Фактическое создание (вызывается после подтверждения)
const executeCreate = async () => {
try {
setIsLoading(true);
await createStation();
const data = await createStation();
toast.success("Остановка успешно создана");
navigate("/station");
navigate(`/station/${data.id}/edit`);
} catch (error) {
console.error("Error creating station:", error);
toast.error("Ошибка при создании станции");
toast.error("Ошибка при создании остановки");
} finally {
setIsLoading(false);
}
};
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или создания
const handleCreate = async () => {
const isCityMissing = !createStationData.common.city_id;
// Проверяем названия на всех языках
const isNameMissing = !createStationData.ru.name || !createStationData.en.name || !createStationData.zh.name;
const isNameMissing =
!createStationData.ru.name ||
!createStationData.en.name ||
!createStationData.zh.name;
if (isCityMissing || isNameMissing) {
setIsSaveWarningOpen(true);
@@ -78,28 +102,64 @@ export const StationCreatePage = observer(() => {
await executeCreate();
};
// Обработчик "Да" в предупреждающем окне
const handleConfirmCreate = async () => {
setIsSaveWarningOpen(false);
await executeCreate();
};
// Обработчик "Нет" в предупреждающем окне
const handleCancelCreate = () => {
setIsSaveWarningOpen(false);
};
useEffect(() => {
const fetchCities = async () => {
await getCities("ru");
await getCities("en");
await getCities("zh");
if (!authStore.me) {
await authStore.getMeAction().catch(() => undefined);
}
if (authStore.canRead("cities")) {
await getCities("ru");
return;
}
await authStore.fetchMeCities().catch(() => undefined);
};
fetchCities();
mediaStore.getMedia();
}, []);
// Автоматически устанавливаем выбранный город при загрузке страницы
const baseCities = canReadCities
? cityStore.cities["ru"].data
: authStore.meCities["ru"].map((city) => ({
id: city.city_id,
name: city.name,
country: "",
country_code: "",
arms: "",
}));
const availableCities =
selectedCity?.id && !baseCities.some((city) => city.id === selectedCity.id)
? [selectedCity, ...baseCities]
: baseCities;
const handleMediaSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
setCreateCommonData({ icon: media.id });
};
const selectedMedia =
createStationData.common.icon &&
!isMediaIdEmpty(createStationData.common.icon)
? mediaStore.media.find((m) => m.id === createStationData.common.icon)
: null;
const effectiveIconUrl = isMediaIdEmpty(createStationData.common.icon)
? null
: selectedMedia?.id ?? createStationData.common.icon;
useEffect(() => {
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
setCreateCommonData({
@@ -110,7 +170,7 @@ export const StationCreatePage = observer(() => {
}, [selectedCityId, selectedCity, createStationData.common.city_id]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<Box className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
@@ -138,23 +198,6 @@ export const StationCreatePage = observer(() => {
}
/>
<FormControl fullWidth>
<InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel>
<Select
labelId="direction-label"
value={createStationData.common.direction ? "Прямой" : "Обратный"}
label="Прямой/обратный маршрут"
onChange={(e) =>
setCreateCommonData({
direction: e.target.value === "Прямой",
})
}
>
<MenuItem value="Прямой">Прямой</MenuItem>
<MenuItem value="Обратный">Обратный</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
label="Описание"
@@ -215,7 +258,7 @@ export const StationCreatePage = observer(() => {
value={createStationData.common.city_id || ""}
label="Город"
onChange={(e) => {
const selectedCity = cities["ru"].data.find(
const selectedCity = availableCities.find(
(city) => city.id === e.target.value
);
setCreateCommonData({
@@ -224,7 +267,7 @@ export const StationCreatePage = observer(() => {
});
}}
>
{cities["ru"].data.map((city) => (
{availableCities.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
@@ -232,12 +275,36 @@ export const StationCreatePage = observer(() => {
</Select>
</FormControl>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
<ImageUploadCard
title="Иконка остановки"
imageKey="thumbnail"
imageUrl={effectiveIconUrl}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(effectiveIconUrl ?? "");
}}
onDeleteImageClick={() => {
setCreateCommonData({ icon: "" });
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("image");
}}
/>
</div>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleCreate
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
@@ -248,6 +315,28 @@ export const StationCreatePage = observer(() => {
</div>
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
<SelectMediaDialog
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={1}
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
contextObjectName={createStationData[language].name || "Остановка"}
contextType="station"
afterUpload={handleMediaSelect}
hardcodeType={activeMenuType}
/>
<PreviewMediaDialog
open={isPreviewMediaOpen}
onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId}
/>
{isSaveWarningOpen && (
<SaveWithoutCityAgree
blocker={{
@@ -256,6 +345,6 @@ export const StationCreatePage = observer(() => {
}}
/>
)}
</Paper>
</Box>
);
});