feat(backup): 实现自动备份功能及错误边界处理
This commit is contained in:
@@ -12,6 +12,7 @@ const envMapUrl = '/assets/3d/env.hdr';
|
||||
import RackModel from './RackModel';
|
||||
import { useScene3D } from '../../context/Scene3DContext';
|
||||
import * as THREE from 'three';
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
|
||||
// 检测是否为移动设备
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
@@ -153,50 +154,56 @@ const Scene = forwardRef(
|
||||
}, [rackHeightMeters]);
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={deviceDpr}
|
||||
performance={{ min: 0.5 }}
|
||||
gl={{
|
||||
antialias: true, // 对所有设备开启抗锯齿提升清晰度
|
||||
alpha: true, // 必须开启alpha以支持透明背景
|
||||
powerPreference: 'high-performance',
|
||||
}}
|
||||
style={{ background: 'transparent' }}
|
||||
<ErrorBoundary
|
||||
fullPage={false}
|
||||
title="3D 场景渲染失败"
|
||||
subTitle="3D 场景在渲染过程中遇到错误,请检查浏览器是否支持 WebGL"
|
||||
>
|
||||
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
||||
|
||||
<ambientLight intensity={0.5} color="#ffffff" />
|
||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
||||
<directionalLight
|
||||
position={[10, 10, 5]}
|
||||
intensity={1}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-far={20}
|
||||
shadow-camera-left={-10}
|
||||
shadow-camera-right={10}
|
||||
shadow-camera-top={10}
|
||||
shadow-camera-bottom={-10}
|
||||
/>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
|
||||
</Suspense>
|
||||
|
||||
{/* Models */}
|
||||
<group position={[0, 0, 0]}>
|
||||
<RackModel {...rackModelProps} />
|
||||
</group>
|
||||
|
||||
{/* Controls - 使用独立组件保持旋转中心固定 */}
|
||||
<Controls
|
||||
rack={rack}
|
||||
onControlsReady={api => {
|
||||
controlsApiRef.current = api;
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={deviceDpr}
|
||||
performance={{ min: 0.5 }}
|
||||
gl={{
|
||||
antialias: true, // 对所有设备开启抗锯齿提升清晰度
|
||||
alpha: true, // 必须开启 alpha 以支持透明背景
|
||||
powerPreference: 'high-performance',
|
||||
}}
|
||||
/>
|
||||
</Canvas>
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
||||
|
||||
<ambientLight intensity={0.5} color="#ffffff" />
|
||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
||||
<directionalLight
|
||||
position={[10, 10, 5]}
|
||||
intensity={1}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-far={20}
|
||||
shadow-camera-left={-10}
|
||||
shadow-camera-right={10}
|
||||
shadow-camera-top={10}
|
||||
shadow-camera-bottom={-10}
|
||||
/>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
|
||||
</Suspense>
|
||||
|
||||
{/* Models */}
|
||||
<group position={[0, 0, 0]}>
|
||||
<RackModel {...rackModelProps} />
|
||||
</group>
|
||||
|
||||
{/* Controls - 使用独立组件保持旋转中心固定 */}
|
||||
<Controls
|
||||
rack={rack}
|
||||
onControlsReady={api => {
|
||||
controlsApiRef.current = api;
|
||||
}}
|
||||
/>
|
||||
</Canvas>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Button, Result, Space, Collapse, Typography } from 'antd';
|
||||
import {
|
||||
WarningOutlined,
|
||||
ReloadOutlined,
|
||||
HomeOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
|
||||
console.error('错误边界捕获到错误:', error, errorInfo);
|
||||
|
||||
if (this.props.onError) {
|
||||
this.props.onError(error, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleGoHome = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: this.props.fullPage ? '100vh' : '400px',
|
||||
background: this.props.fullPage ? '#f5f5f5' : 'transparent',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="error"
|
||||
title={this.props.title || '组件加载失败'}
|
||||
subTitle={
|
||||
this.props.subTitle ||
|
||||
'抱歉,该组件在加载过程中遇到了错误。您可以尝试重新加载或返回首页。'
|
||||
}
|
||||
icon={
|
||||
<WarningOutlined
|
||||
style={{
|
||||
fontSize: '64px',
|
||||
color: '#ff4d4f',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
extra={
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={this.handleReload}
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
<Button
|
||||
icon={<HomeOutlined />}
|
||||
onClick={this.handleGoHome}
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{this.state.error && (
|
||||
<div style={{ textAlign: 'left', marginTop: '24px' }}>
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'error-details',
|
||||
label: (
|
||||
<Space>
|
||||
<BugOutlined />
|
||||
<Text type="secondary">查看错误详情(开发环境)</Text>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
<Paragraph
|
||||
code
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
{this.state.error.toString()}
|
||||
</Paragraph>
|
||||
{this.state.errorInfo?.componentStack && (
|
||||
<Paragraph
|
||||
code
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '200px',
|
||||
}}
|
||||
>
|
||||
{this.state.errorInfo.componentStack}
|
||||
</Paragraph>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Result>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.defaultProps = {
|
||||
fallback: null,
|
||||
fullPage: false,
|
||||
title: null,
|
||||
subTitle: null,
|
||||
onError: null,
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -103,7 +103,7 @@ const DeviceFormModal = ({
|
||||
style={inputStyle}
|
||||
className="form-input-enhanced"
|
||||
>
|
||||
{field.options &&
|
||||
{Array.isArray(field.options) &&
|
||||
field.options.map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
|
||||
Reference in New Issue
Block a user