241 lines
7.3 KiB
TypeScript
241 lines
7.3 KiB
TypeScript
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) {
|
||
return {
|
||
hasError: false,
|
||
error: null,
|
||
lastResetKey: props.resetKey,
|
||
};
|
||
}
|
||
|
||
// Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey
|
||
// но не сбрасываем ошибку автоматически - ждем результата загрузки
|
||
|
||
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 = () => {
|
||
// Сначала сбрасываем состояние ошибки
|
||
this.setState(
|
||
{
|
||
hasError: false,
|
||
error: null,
|
||
},
|
||
() => {
|
||
// После того как состояние обновилось, вызываем callback для изменения resetKey
|
||
// Это приведет к пересозданию компонента и новой попытке загрузки
|
||
this.props.onReset?.();
|
||
}
|
||
);
|
||
};
|
||
|
||
handleClose = () => {
|
||
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={() => {
|
||
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;
|
||
}
|
||
}
|