mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 18:52:12 +08:00
chore: reorganize frontend files
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
ErrorOutlineRounded,
|
||||
RefreshRounded,
|
||||
BugReportRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
|
||||
import React, { Component, ErrorInfo, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallbackComponent?: ReactNode;
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
showDetails: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流量统计专用错误边界组件
|
||||
* 处理图表和流量统计组件的错误,提供优雅的降级体验
|
||||
*/
|
||||
export class TrafficErrorBoundary extends Component<Props, State> {
|
||||
private retryCount = 0;
|
||||
private maxRetries = 3;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
showDetails: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
// 更新状态以显示降级UI
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("[TrafficErrorBoundary] 捕获到组件错误:", error, errorInfo);
|
||||
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
|
||||
// 调用错误回调
|
||||
if (this.props.onError) {
|
||||
this.props.onError(error, errorInfo);
|
||||
}
|
||||
|
||||
// 发送错误到监控系统(如果有的话)
|
||||
this.reportError(error, errorInfo);
|
||||
}
|
||||
|
||||
private reportError = (error: Error, errorInfo: ErrorInfo) => {
|
||||
// 这里可以集成错误监控服务
|
||||
const errorReport = {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent,
|
||||
url: window.location.href,
|
||||
};
|
||||
|
||||
console.error("[TrafficErrorBoundary] 错误报告:", errorReport);
|
||||
// TODO: 发送到错误监控服务
|
||||
// sendErrorReport(errorReport);
|
||||
};
|
||||
|
||||
private handleRetry = () => {
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
this.retryCount++;
|
||||
console.log(
|
||||
`[TrafficErrorBoundary] 尝试重试 (${this.retryCount}/${this.maxRetries})`,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
showDetails: false,
|
||||
});
|
||||
} else {
|
||||
console.warn("[TrafficErrorBoundary] 已达到最大重试次数");
|
||||
}
|
||||
};
|
||||
|
||||
private handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
private toggleDetails = () => {
|
||||
this.setState((prev) => ({ showDetails: !prev.showDetails }));
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// 如果提供了自定义降级组件,使用它
|
||||
if (this.props.fallbackComponent) {
|
||||
return this.props.fallbackComponent;
|
||||
}
|
||||
|
||||
// 默认错误UI
|
||||
return (
|
||||
<TrafficErrorFallback
|
||||
error={this.state.error}
|
||||
errorInfo={this.state.errorInfo}
|
||||
showDetails={this.state.showDetails}
|
||||
canRetry={this.retryCount < this.maxRetries}
|
||||
retryCount={this.retryCount}
|
||||
maxRetries={this.maxRetries}
|
||||
onRetry={this.handleRetry}
|
||||
onRefresh={this.handleRefresh}
|
||||
onToggleDetails={this.toggleDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误降级UI组件
|
||||
*/
|
||||
interface TrafficErrorFallbackProps {
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
showDetails: boolean;
|
||||
canRetry: boolean;
|
||||
retryCount: number;
|
||||
maxRetries: number;
|
||||
onRetry: () => void;
|
||||
onRefresh: () => void;
|
||||
onToggleDetails: () => void;
|
||||
}
|
||||
|
||||
const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
||||
error,
|
||||
errorInfo,
|
||||
showDetails,
|
||||
canRetry,
|
||||
retryCount,
|
||||
maxRetries,
|
||||
onRetry,
|
||||
onRefresh,
|
||||
onToggleDetails,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
minHeight: 200,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "1px dashed",
|
||||
borderColor: "error.main",
|
||||
borderRadius: 2,
|
||||
bgcolor: "error.light",
|
||||
color: "error.contrastText",
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("shared.feedback.errors.trafficStats")}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
textAlign="center"
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{t("shared.feedback.errors.trafficStatsDescription")}
|
||||
</Typography>
|
||||
|
||||
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Error:</strong>{" "}
|
||||
{error instanceof Error ? error.message : "Unknown error"}
|
||||
</Typography>
|
||||
{retryCount > 0 && (
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||
{t("shared.labels.retryAttempts")}: {retryCount}/{maxRetries}
|
||||
</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1, mb: 2 }}>
|
||||
{canRetry && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<RefreshRounded />}
|
||||
onClick={onRetry}
|
||||
size="small"
|
||||
>
|
||||
{t("shared.actions.retry")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="outlined" onClick={onRefresh} size="small">
|
||||
{t("shared.actions.refreshPage")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<BugReportRounded />}
|
||||
onClick={onToggleDetails}
|
||||
size="small"
|
||||
>
|
||||
{showDetails
|
||||
? t("shared.actions.hideDetails")
|
||||
: t("shared.actions.showDetails")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Collapse in={showDetails} sx={{ width: "100%", maxWidth: 600 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: 1,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Error Details:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="pre"
|
||||
sx={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.75rem",
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
{error?.stack}
|
||||
</Typography>
|
||||
|
||||
{errorInfo?.componentStack && (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
|
||||
Component Stack:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="pre"
|
||||
sx={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.75rem",
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
{errorInfo.componentStack}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 轻量级流量统计错误边界
|
||||
* 用于小型流量显示组件,提供最小化的错误UI
|
||||
*/
|
||||
export const LightweightTrafficErrorBoundary: React.FC<{
|
||||
children: ReactNode;
|
||||
}> = ({ children }) => {
|
||||
return (
|
||||
<TrafficErrorBoundary
|
||||
fallbackComponent={
|
||||
<Box
|
||||
sx={{
|
||||
p: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 60,
|
||||
bgcolor: "error.light",
|
||||
borderRadius: 1,
|
||||
color: "error.contrastText",
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
|
||||
<Typography variant="caption">Traffic data unavailable</Typography>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</TrafficErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import type { ErrorInfo } from "react";
|
||||
|
||||
import {
|
||||
TrafficErrorBoundary,
|
||||
LightweightTrafficErrorBoundary,
|
||||
} from "./traffic-error-boundary";
|
||||
|
||||
interface WithTrafficErrorBoundaryOptions {
|
||||
lightweight?: boolean;
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* HOC:为任何组件添加流量错误边界
|
||||
*/
|
||||
export function withTrafficErrorBoundary<P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
options?: WithTrafficErrorBoundaryOptions,
|
||||
) {
|
||||
const WithErrorBoundaryComponent = (props: P) => {
|
||||
const ErrorBoundaryComponent = options?.lightweight
|
||||
? LightweightTrafficErrorBoundary
|
||||
: TrafficErrorBoundary;
|
||||
|
||||
return (
|
||||
<ErrorBoundaryComponent onError={options?.onError}>
|
||||
<WrappedComponent {...props} />
|
||||
</ErrorBoundaryComponent>
|
||||
);
|
||||
};
|
||||
|
||||
WithErrorBoundaryComponent.displayName = `withTrafficErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name})`;
|
||||
|
||||
return WithErrorBoundaryComponent;
|
||||
}
|
||||
Reference in New Issue
Block a user