fix: Fix 3d models
This commit is contained in:
158
src/app/GlobalErrorBoundary.tsx
Normal file
158
src/app/GlobalErrorBoundary.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { Box, Button, Typography, Paper, Container } from "@mui/material";
|
||||
import { RefreshCw, Home, AlertTriangle } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class GlobalErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error("❌ GlobalErrorBoundary: Критическая ошибка приложения", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
});
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
});
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleGoHome = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
});
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "background.default",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="sm">
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={64} color="#f44336" />
|
||||
</Box>
|
||||
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Упс! Что-то пошло не так
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Приложение столкнулось с неожиданной ошибкой. Попробуйте
|
||||
перезагрузить страницу или вернуться на главную.
|
||||
</Typography>
|
||||
|
||||
{this.state.error?.message && (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 3,
|
||||
backgroundColor: "error.light",
|
||||
color: "error.contrastText",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: "bold", display: "block", mb: 1 }}
|
||||
>
|
||||
Информация об ошибке:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.75rem",
|
||||
wordBreak: "break-word",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{this.state.error.message}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 2,
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Home size={16} />}
|
||||
onClick={this.handleGoHome}
|
||||
size="large"
|
||||
>
|
||||
На главную
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshCw size={16} />}
|
||||
onClick={this.handleReset}
|
||||
size="large"
|
||||
>
|
||||
Перезагрузить
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import { Router } from "./router";
|
||||
import { CustomTheme } from "@shared";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { GlobalErrorBoundary } from "./GlobalErrorBoundary";
|
||||
|
||||
export const App: React.FC = () => (
|
||||
<ThemeProvider theme={CustomTheme.Light}>
|
||||
<ToastContainer />
|
||||
<Router />
|
||||
</ThemeProvider>
|
||||
<GlobalErrorBoundary>
|
||||
<ThemeProvider theme={CustomTheme.Light}>
|
||||
<ToastContainer />
|
||||
<Router />
|
||||
</ThemeProvider>
|
||||
</GlobalErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ export const MEDIA_TYPE_LABELS = {
|
||||
6: "3Д-модель",
|
||||
};
|
||||
|
||||
export * from "./mediaTypes";
|
||||
|
||||
export const MEDIA_TYPE_VALUES = {
|
||||
image: 1,
|
||||
video: 2,
|
||||
|
||||
85
src/shared/const/mediaTypes.ts
Normal file
85
src/shared/const/mediaTypes.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// Допустимые типы и расширения файлов для медиа
|
||||
export const ALLOWED_MEDIA_TYPES = {
|
||||
image: {
|
||||
extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"],
|
||||
mimeTypes: [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/svg+xml",
|
||||
],
|
||||
accept: "image/*",
|
||||
},
|
||||
video: {
|
||||
extensions: [".mp4", ".webm", ".ogg", ".mov", ".avi"],
|
||||
mimeTypes: [
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/ogg",
|
||||
"video/quicktime",
|
||||
"video/x-msvideo",
|
||||
],
|
||||
accept: "video/*",
|
||||
},
|
||||
model3d: {
|
||||
extensions: [".glb", ".gltf"],
|
||||
mimeTypes: ["model/gltf-binary", "model/gltf+json"],
|
||||
accept: ".glb,.gltf",
|
||||
},
|
||||
panorama: {
|
||||
extensions: [".jpg", ".jpeg", ".png"],
|
||||
mimeTypes: ["image/jpeg", "image/png"],
|
||||
accept: "image/*",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const getAllAllowedExtensions = (): string[] => {
|
||||
return [
|
||||
...ALLOWED_MEDIA_TYPES.image.extensions,
|
||||
...ALLOWED_MEDIA_TYPES.video.extensions,
|
||||
...ALLOWED_MEDIA_TYPES.model3d.extensions,
|
||||
];
|
||||
};
|
||||
|
||||
export const getAllAcceptString = (): string => {
|
||||
return `${ALLOWED_MEDIA_TYPES.image.accept},${ALLOWED_MEDIA_TYPES.video.accept},${ALLOWED_MEDIA_TYPES.model3d.accept}`;
|
||||
};
|
||||
|
||||
export const validateFileExtension = (
|
||||
file: File
|
||||
): { valid: boolean; error?: string } => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
const extension = fileName.substring(fileName.lastIndexOf("."));
|
||||
const allowedExtensions = getAllAllowedExtensions();
|
||||
|
||||
if (!allowedExtensions.includes(extension)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Неподдерживаемый формат файла "${extension}". Допустимые форматы: ${allowedExtensions.join(
|
||||
", "
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
export const filterValidFiles = (
|
||||
files: File[]
|
||||
): { validFiles: File[]; errors: string[] } => {
|
||||
const validFiles: File[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
const validation = validateFileExtension(file);
|
||||
if (validation.valid) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
errors.push(`${file.name}: ${validation.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
return { validFiles, errors };
|
||||
};
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { PreviewMediaDialog } from "@shared";
|
||||
import {
|
||||
PreviewMediaDialog,
|
||||
filterValidFiles,
|
||||
getAllAcceptString,
|
||||
} from "@shared";
|
||||
import { X, Upload } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, DragEvent, useRef } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const MediaArea = observer(
|
||||
({
|
||||
@@ -36,7 +41,15 @@ export const MediaArea = observer(
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length && onFilesDrop) {
|
||||
onFilesDrop(files);
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onFilesDrop(validFiles);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,7 +69,15 @@ export const MediaArea = observer(
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length && onFilesDrop) {
|
||||
onFilesDrop(files);
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onFilesDrop(validFiles);
|
||||
}
|
||||
}
|
||||
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
|
||||
event.target.value = "";
|
||||
@@ -68,7 +89,7 @@ export const MediaArea = observer(
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,video/*,.glb,.gltf"
|
||||
accept={getAllAcceptString()}
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { editSightStore, SelectMediaDialog, UploadMediaDialog } from "@shared";
|
||||
import {
|
||||
editSightStore,
|
||||
SelectMediaDialog,
|
||||
UploadMediaDialog,
|
||||
filterValidFiles,
|
||||
getAllAcceptString,
|
||||
} from "@shared";
|
||||
import { Upload } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, DragEvent, useRef } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const MediaAreaForSight = observer(
|
||||
({
|
||||
@@ -38,11 +45,18 @@ export const MediaAreaForSight = observer(
|
||||
setIsDragging(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length && onFilesDrop) {
|
||||
setFileToUpload(files[0]);
|
||||
}
|
||||
if (files.length) {
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
setUploadMediaDialogOpen(true);
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error: string) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0 && onFilesDrop) {
|
||||
setFileToUpload(validFiles[0]);
|
||||
setUploadMediaDialogOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
@@ -60,10 +74,18 @@ export const MediaAreaForSight = observer(
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length && onFilesDrop) {
|
||||
setFileToUpload(files[0]);
|
||||
onFilesDrop(files);
|
||||
setUploadMediaDialogOpen(true);
|
||||
if (files.length) {
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error: string) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0 && onFilesDrop) {
|
||||
setFileToUpload(validFiles[0]);
|
||||
onFilesDrop(validFiles);
|
||||
setUploadMediaDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
|
||||
@@ -76,7 +98,7 @@ export const MediaAreaForSight = observer(
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,video/*,.glb,.gltf"
|
||||
accept={getAllAcceptString()}
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
||||
import { useEffect, Suspense } from "react";
|
||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||
|
||||
type ModelViewerProps = {
|
||||
width?: string;
|
||||
@@ -7,21 +9,72 @@ type ModelViewerProps = {
|
||||
height?: string;
|
||||
};
|
||||
|
||||
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
||||
const { scene } = useGLTF(fileUrl);
|
||||
return <primitive object={scene} />;
|
||||
};
|
||||
|
||||
const LoadingFallback = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
zIndex: 1000,
|
||||
backgroundColor: "background.paper",
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={48} />
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
Загрузка 3D модели...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreeView = ({
|
||||
fileUrl,
|
||||
height = "100%",
|
||||
width = "100%",
|
||||
}: ModelViewerProps) => {
|
||||
const { scene } = useGLTF(fileUrl);
|
||||
// Очищаем кеш при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
useGLTF.clear(fileUrl);
|
||||
console.log("🧹 ThreeView: Очистка кеша модели", { fileUrl });
|
||||
};
|
||||
}, [fileUrl]);
|
||||
|
||||
return (
|
||||
<Canvas style={{ height: height, width: width }}>
|
||||
<ambientLight />
|
||||
<directionalLight />
|
||||
<Stage environment="city" intensity={0.6}>
|
||||
<primitive object={scene} />
|
||||
</Stage>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
<Box sx={{ position: "relative", width, height }}>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Canvas
|
||||
style={{ height: height, width: width }}
|
||||
camera={{
|
||||
position: [1, 1, 1],
|
||||
fov: 30,
|
||||
}}
|
||||
>
|
||||
<ambientLight />
|
||||
<directionalLight />
|
||||
<Stage environment="city" intensity={0.6} adjustCamera={false}>
|
||||
<Model fileUrl={fileUrl} />
|
||||
</Stage>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
</Suspense>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
263
src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx
Normal file
263
src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { Box, Button, Typography, Paper } from "@mui/material";
|
||||
import { RefreshCw, AlertTriangle } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
onReset?: () => void;
|
||||
resetKey?: number | string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
lastResetKey?: number | string;
|
||||
}
|
||||
|
||||
export class ThreeViewErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
lastResetKey: props.resetKey,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(
|
||||
props: Props,
|
||||
state: State
|
||||
): Partial<State> | null {
|
||||
// Сбрасываем ошибку ТОЛЬКО при смене медиа (когда меняется ID в resetKey)
|
||||
if (
|
||||
props.resetKey !== state.lastResetKey &&
|
||||
state.lastResetKey !== undefined
|
||||
) {
|
||||
const oldMediaId = String(state.lastResetKey).split("-")[0];
|
||||
const newMediaId = String(props.resetKey).split("-")[0];
|
||||
|
||||
// Сбрасываем ошибку только если изменился ID медиа (пользователь выбрал другую модель)
|
||||
if (oldMediaId !== newMediaId) {
|
||||
console.log(
|
||||
"🔄 ThreeViewErrorBoundary: Сброс ошибки при смене модели",
|
||||
{
|
||||
oldKey: state.lastResetKey,
|
||||
newKey: props.resetKey,
|
||||
oldMediaId,
|
||||
newMediaId,
|
||||
}
|
||||
);
|
||||
return {
|
||||
hasError: false,
|
||||
error: null,
|
||||
lastResetKey: props.resetKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey
|
||||
// но не сбрасываем ошибку автоматически - ждем результата загрузки
|
||||
console.log(
|
||||
"🔄 ThreeViewErrorBoundary: Обновление lastResetKey без сброса ошибки",
|
||||
{
|
||||
oldKey: state.lastResetKey,
|
||||
newKey: props.resetKey,
|
||||
}
|
||||
);
|
||||
return {
|
||||
lastResetKey: props.resetKey,
|
||||
};
|
||||
}
|
||||
|
||||
if (state.lastResetKey === undefined && props.resetKey !== undefined) {
|
||||
return {
|
||||
lastResetKey: props.resetKey,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error("❌ ThreeViewErrorBoundary: Ошибка загрузки 3D модели", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
});
|
||||
}
|
||||
|
||||
getErrorMessage = () => {
|
||||
const errorMessage = this.state.error?.message || "";
|
||||
|
||||
if (
|
||||
errorMessage.includes("not valid JSON") ||
|
||||
errorMessage.includes("Unexpected token")
|
||||
) {
|
||||
return "Неверный формат файла. Убедитесь, что файл является корректной 3D моделью в формате GLB/GLTF.";
|
||||
}
|
||||
|
||||
if (errorMessage.includes("Could not load")) {
|
||||
return "Не удалось загрузить файл 3D модели. Проверьте, что файл существует и доступен.";
|
||||
}
|
||||
|
||||
if (errorMessage.includes("404") || errorMessage.includes("Not Found")) {
|
||||
return "Файл 3D модели не найден на сервере.";
|
||||
}
|
||||
|
||||
if (errorMessage.includes("Network") || errorMessage.includes("fetch")) {
|
||||
return "Ошибка сети при загрузке 3D модели. Проверьте интернет-соединение.";
|
||||
}
|
||||
|
||||
return (
|
||||
errorMessage || "Произошла неизвестная ошибка при загрузке 3D модели"
|
||||
);
|
||||
};
|
||||
|
||||
getErrorReasons = () => {
|
||||
const errorMessage = this.state.error?.message || "";
|
||||
|
||||
if (
|
||||
errorMessage.includes("not valid JSON") ||
|
||||
errorMessage.includes("Unexpected token")
|
||||
) {
|
||||
return [
|
||||
"Файл не является 3D моделью",
|
||||
"Загружен файл неподдерживаемого формата",
|
||||
"Файл поврежден или не полностью загружен",
|
||||
"Используйте только GLB или GLTF форматы",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"Поврежденный файл модели",
|
||||
"Неподдерживаемый формат",
|
||||
"Проблемы с загрузкой файла",
|
||||
];
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
console.log(
|
||||
"🔄 ThreeViewErrorBoundary: Перезагрузка компонента и перезапрос модели"
|
||||
);
|
||||
|
||||
// Сначала сбрасываем состояние ошибки
|
||||
this.setState(
|
||||
{
|
||||
hasError: false,
|
||||
error: null,
|
||||
},
|
||||
() => {
|
||||
// После того как состояние обновилось, вызываем callback для изменения resetKey
|
||||
// Это приведет к пересозданию компонента и новой попытке загрузки
|
||||
this.props.onReset?.();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
console.log("❌ ThreeViewErrorBoundary: Закрытие ошибки");
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 3,
|
||||
maxWidth: 500,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
backgroundColor: "error.light",
|
||||
color: "error.contrastText",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||||
<AlertTriangle size={32} style={{ marginRight: 12 }} />
|
||||
<Typography variant="h6" component="h2">
|
||||
Ошибка загрузки 3D модели
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
{this.getErrorMessage()}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" sx={{ mb: 2, display: "block" }}>
|
||||
Возможные причины:
|
||||
<ul style={{ margin: "8px 0", paddingLeft: "20px" }}>
|
||||
{this.getErrorReasons().map((reason, index) => (
|
||||
<li key={index}>{reason}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Typography>
|
||||
|
||||
{this.state.error?.message && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
mb: 2,
|
||||
display: "block",
|
||||
fontFamily: "monospace",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
fontSize: "0.7rem",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: "100px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{this.state.error.message}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshCw size={16} />}
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"🖱️ ThreeViewErrorBoundary: Клик на кнопку 'Попробовать снова'"
|
||||
);
|
||||
this.handleReset();
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: "error.contrastText",
|
||||
color: "error.main",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Попробовать снова
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
|
||||
import { ThreeView } from "./ThreeView";
|
||||
import { ThreeViewErrorBoundary } from "./ThreeViewErrorBoundary";
|
||||
|
||||
export interface MediaData {
|
||||
id: string | number;
|
||||
@@ -25,6 +27,23 @@ export function MediaViewer({
|
||||
fullHeight?: boolean;
|
||||
}>) {
|
||||
const token = localStorage.getItem("token");
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
|
||||
const handleReset = () => {
|
||||
console.log("🔄 MediaViewer: handleReset вызван", {
|
||||
currentResetKey: resetKey,
|
||||
mediaId: media?.id,
|
||||
});
|
||||
setResetKey((prev) => {
|
||||
const newKey = prev + 1;
|
||||
console.log("🔄 MediaViewer: resetKey обновлен", {
|
||||
oldKey: prev,
|
||||
newKey,
|
||||
});
|
||||
return newKey;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
@@ -108,13 +127,19 @@ export function MediaViewer({
|
||||
)}
|
||||
|
||||
{media?.media_type === 6 && (
|
||||
<ThreeView
|
||||
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
media?.id
|
||||
}/download?token=${token}`}
|
||||
height={height ? height : "500px"}
|
||||
width={width ? width : "500px"}
|
||||
/>
|
||||
<ThreeViewErrorBoundary
|
||||
onReset={handleReset}
|
||||
resetKey={`${media?.id}-${resetKey}`}
|
||||
>
|
||||
<ThreeView
|
||||
key={`3d-model-${media?.id}-${resetKey}`}
|
||||
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
media?.id
|
||||
}/download?token=${token}`}
|
||||
height={height ? height : "500px"}
|
||||
width={width ? width : "500px"}
|
||||
/>
|
||||
</ThreeViewErrorBoundary>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user