638 lines
16 KiB
Dart
638 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// 错误提示组件库 - 提供多种样式的错误状态显示
|
||
/// 包括网络错误、服务器错误、权限错误等不同场景的错误提示
|
||
|
||
/// 基础错误状态组件 - 通用的错误状态显示组件
|
||
/// [errorType] 错误类型,决定图标和颜色
|
||
/// [title] 错误标题文本
|
||
/// [message] 错误描述文本
|
||
/// [actionText] 操作按钮文本,null时不显示按钮
|
||
/// [onAction] 操作按钮点击回调,null时按钮不可点击
|
||
/// [iconSize] 图标大小,null时使用默认大小
|
||
/// [showDetails] 是否显示详细错误信息
|
||
/// [details] 详细错误信息,用于调试
|
||
class ErrorState extends StatelessWidget {
|
||
/// 错误类型,决定图标和颜色
|
||
final ErrorType errorType;
|
||
|
||
/// 错误标题文本
|
||
final String title;
|
||
|
||
/// 错误描述文本
|
||
final String? message;
|
||
|
||
/// 操作按钮文本,null时不显示按钮
|
||
final String? actionText;
|
||
|
||
/// 操作按钮点击回调,null时按钮不可点击
|
||
final VoidCallback? onAction;
|
||
|
||
/// 图标大小,null时使用默认大小
|
||
final double? iconSize;
|
||
|
||
/// 自定义图标组件,优先级高于内置图标
|
||
final Widget? customIcon;
|
||
|
||
/// 自定义操作组件,优先级高于actionText参数
|
||
final Widget? customAction;
|
||
|
||
/// 主要内容垂直间距
|
||
final double spacing;
|
||
|
||
/// 主要内容内边距
|
||
final EdgeInsetsGeometry? padding;
|
||
|
||
/// 是否显示边框装饰
|
||
final bool showBorder;
|
||
|
||
/// 边框圆角半径
|
||
final double borderRadius;
|
||
|
||
/// 背景颜色,null时使用透明背景
|
||
final Color? backgroundColor;
|
||
|
||
/// 是否显示详细错误信息
|
||
final bool showDetails;
|
||
|
||
/// 详细错误信息,用于调试
|
||
final String? details;
|
||
|
||
const ErrorState({
|
||
super.key,
|
||
this.errorType = ErrorType.general,
|
||
required this.title,
|
||
this.message,
|
||
this.actionText,
|
||
this.onAction,
|
||
this.iconSize,
|
||
this.customIcon,
|
||
this.customAction,
|
||
this.spacing = 16,
|
||
this.padding,
|
||
this.showBorder = false,
|
||
this.borderRadius = 12,
|
||
this.backgroundColor,
|
||
this.showDetails = false,
|
||
this.details,
|
||
});
|
||
|
||
/// 创建网络错误状态 - 用于网络连接失败场景
|
||
factory ErrorState.networkError({
|
||
VoidCallback? onRetry,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.network,
|
||
title: '网络连接失败',
|
||
message: '请检查网络连接后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建服务器错误状态 - 用于服务器异常场景
|
||
factory ErrorState.serverError({
|
||
VoidCallback? onRetry,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.server,
|
||
title: '服务器异常',
|
||
message: '服务器暂时无法访问,请稍后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建权限错误状态 - 用于无权限访问场景
|
||
factory ErrorState.permissionError({
|
||
VoidCallback? onRequestPermission,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.permission,
|
||
title: '权限不足',
|
||
message: '您没有访问此内容的权限',
|
||
actionText: onRequestPermission != null ? '申请权限' : null,
|
||
onAction: onRequestPermission,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建数据错误状态 - 用于数据解析失败场景
|
||
factory ErrorState.dataError({
|
||
VoidCallback? onRetry,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.data,
|
||
title: '数据解析失败',
|
||
message: '数据格式错误,无法正确解析',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建超时错误状态 - 用于请求超时场景
|
||
factory ErrorState.timeoutError({
|
||
VoidCallback? onRetry,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.timeout,
|
||
title: '请求超时',
|
||
message: '请求时间过长,请重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建未找到错误状态 - 用于资源未找到场景
|
||
factory ErrorState.notFoundError({
|
||
VoidCallback? onGoBack,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.notFound,
|
||
title: '页面不存在',
|
||
message: '您访问的内容可能已被删除或移动',
|
||
actionText: onGoBack != null ? '返回' : null,
|
||
onAction: onGoBack,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建未知错误状态 - 用于未分类的错误场景
|
||
factory ErrorState.unknownError({
|
||
VoidCallback? onRetry,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorState(
|
||
errorType: ErrorType.unknown,
|
||
title: '发生未知错误',
|
||
message: '抱歉,发生了一些意外情况',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 获取错误类型对应的图标
|
||
IconData _getErrorIcon() {
|
||
switch (errorType) {
|
||
case ErrorType.network:
|
||
return Icons.wifi_off_outlined;
|
||
case ErrorType.server:
|
||
return Icons.cloud_off_outlined;
|
||
case ErrorType.permission:
|
||
return Icons.lock_outline;
|
||
case ErrorType.data:
|
||
return Icons.data_array_outlined;
|
||
case ErrorType.timeout:
|
||
return Icons.timer_off_outlined;
|
||
case ErrorType.notFound:
|
||
return Icons.search_off_outlined;
|
||
case ErrorType.unknown:
|
||
default:
|
||
return Icons.error_outline;
|
||
}
|
||
}
|
||
|
||
/// 获取错误类型对应的颜色
|
||
Color _getErrorColor(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
switch (errorType) {
|
||
case ErrorType.network:
|
||
return Colors.orange;
|
||
case ErrorType.server:
|
||
return Colors.red;
|
||
case ErrorType.permission:
|
||
return Colors.orange;
|
||
case ErrorType.data:
|
||
return Colors.red;
|
||
case ErrorType.timeout:
|
||
return Colors.orange;
|
||
case ErrorType.notFound:
|
||
return Colors.grey;
|
||
case ErrorType.unknown:
|
||
default:
|
||
return theme.colorScheme.error;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final errorColor = _getErrorColor(context);
|
||
final errorIcon = _getErrorIcon();
|
||
|
||
Widget content = Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
// 图标部分
|
||
if (customIcon != null) ...[
|
||
customIcon!,
|
||
SizedBox(height: spacing),
|
||
] else ...[
|
||
Icon(
|
||
errorIcon,
|
||
size: iconSize ?? 64,
|
||
color: errorColor,
|
||
),
|
||
SizedBox(height: spacing),
|
||
],
|
||
|
||
// 标题部分
|
||
Text(
|
||
title,
|
||
style: theme.textTheme.titleLarge?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
color: errorColor,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
|
||
// 描述文本部分
|
||
if (message != null) ...[
|
||
SizedBox(height: spacing * 0.5),
|
||
Text(
|
||
message!,
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
|
||
// 详细错误信息部分
|
||
if (showDetails && details != null) ...[
|
||
SizedBox(height: spacing * 0.5),
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: errorColor.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: errorColor.withOpacity(0.3),
|
||
width: 1,
|
||
),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'详细信息:',
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: errorColor,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
details!,
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
|
||
// 操作按钮部分
|
||
if (customAction != null) ...[
|
||
SizedBox(height: spacing * 1.5),
|
||
customAction!,
|
||
] else if (actionText != null) ...[
|
||
SizedBox(height: spacing * 1.5),
|
||
ElevatedButton.icon(
|
||
onPressed: onAction,
|
||
icon: const Icon(Icons.refresh, size: 18),
|
||
label: Text(actionText!),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: errorColor,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
);
|
||
|
||
// 添加内边距
|
||
content = Padding(
|
||
padding: padding ?? const EdgeInsets.all(24),
|
||
child: content,
|
||
);
|
||
|
||
// 添加边框装饰
|
||
if (showBorder) {
|
||
content = Container(
|
||
decoration: BoxDecoration(
|
||
color: backgroundColor ?? errorColor.withOpacity(0.05),
|
||
border: Border.all(
|
||
color: errorColor.withOpacity(0.3),
|
||
width: 1,
|
||
),
|
||
borderRadius: BorderRadius.circular(borderRadius),
|
||
),
|
||
child: content,
|
||
);
|
||
} else if (backgroundColor != null) {
|
||
content = Container(
|
||
decoration: BoxDecoration(
|
||
color: backgroundColor,
|
||
borderRadius: BorderRadius.circular(borderRadius),
|
||
),
|
||
child: content,
|
||
);
|
||
}
|
||
|
||
return content;
|
||
}
|
||
}
|
||
|
||
/// 错误状态枚举 - 定义不同类型的错误
|
||
enum ErrorType {
|
||
/// 一般错误 - 通用错误类型
|
||
general,
|
||
|
||
/// 网络错误 - 网络连接问题
|
||
network,
|
||
|
||
/// 服务器错误 - 服务端异常
|
||
server,
|
||
|
||
/// 权限错误 - 权限不足
|
||
permission,
|
||
|
||
/// 数据错误 - 数据解析或格式错误
|
||
data,
|
||
|
||
/// 超时错误 - 请求超时
|
||
timeout,
|
||
|
||
/// 未找到错误 - 资源不存在
|
||
notFound,
|
||
|
||
/// 未知错误 - 未分类的错误
|
||
unknown,
|
||
}
|
||
|
||
/// 错误状态页面 - 全屏错误状态显示,包含图标、文本和操作按钮
|
||
/// [errorType] 错误类型
|
||
/// [title] 错误标题
|
||
/// [message] 错误描述
|
||
/// [actionText] 操作按钮文本
|
||
/// [onAction] 操作按钮点击回调
|
||
/// [showAppBar] 是否显示应用栏
|
||
/// [appBarTitle] 应用栏标题
|
||
/// [showDetails] 是否显示详细错误信息
|
||
/// [details] 详细错误信息
|
||
class ErrorStatePage extends StatelessWidget {
|
||
/// 错误类型
|
||
final ErrorType errorType;
|
||
|
||
/// 错误标题
|
||
final String title;
|
||
|
||
/// 错误描述
|
||
final String? message;
|
||
|
||
/// 操作按钮文本
|
||
final String? actionText;
|
||
|
||
/// 操作按钮点击回调
|
||
final VoidCallback? onAction;
|
||
|
||
/// 是否显示应用栏
|
||
final bool showAppBar;
|
||
|
||
/// 应用栏标题
|
||
final String? appBarTitle;
|
||
|
||
/// 自定义错误状态组件,优先级高于其他参数
|
||
final Widget? customErrorState;
|
||
|
||
/// 背景颜色,null时使用主题背景色
|
||
final Color? backgroundColor;
|
||
|
||
/// 是否显示详细错误信息
|
||
final bool showDetails;
|
||
|
||
/// 详细错误信息,用于调试
|
||
final String? details;
|
||
|
||
const ErrorStatePage({
|
||
super.key,
|
||
this.errorType = ErrorType.general,
|
||
required this.title,
|
||
this.message,
|
||
this.actionText,
|
||
this.onAction,
|
||
this.showAppBar = false,
|
||
this.appBarTitle,
|
||
this.customErrorState,
|
||
this.backgroundColor,
|
||
this.showDetails = false,
|
||
this.details,
|
||
});
|
||
|
||
/// 创建网络错误状态页面
|
||
factory ErrorStatePage.networkError({
|
||
VoidCallback? onRetry,
|
||
bool showAppBar = false,
|
||
String? appBarTitle,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorStatePage(
|
||
errorType: ErrorType.network,
|
||
title: '网络连接失败',
|
||
message: '请检查网络连接后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showAppBar: showAppBar,
|
||
appBarTitle: appBarTitle ?? '网络错误',
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
/// 创建服务器错误状态页面
|
||
factory ErrorStatePage.serverError({
|
||
VoidCallback? onRetry,
|
||
bool showAppBar = false,
|
||
String? appBarTitle,
|
||
String? details,
|
||
bool showDetails = false,
|
||
}) {
|
||
return ErrorStatePage(
|
||
errorType: ErrorType.server,
|
||
title: '服务器异常',
|
||
message: '服务器暂时无法访问,请稍后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showAppBar: showAppBar,
|
||
appBarTitle: appBarTitle ?? '服务器错误',
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
return Scaffold(
|
||
backgroundColor: backgroundColor ?? theme.colorScheme.background,
|
||
appBar: showAppBar
|
||
? AppBar(
|
||
title: Text(appBarTitle ?? title),
|
||
centerTitle: true,
|
||
)
|
||
: null,
|
||
body: Center(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(24),
|
||
child: customErrorState ?? ErrorState(
|
||
errorType: errorType,
|
||
title: title,
|
||
message: message,
|
||
actionText: actionText,
|
||
onAction: onAction,
|
||
showBorder: false,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 错误状态构建器 - 根据错误状态显示不同内容
|
||
/// [hasError] 是否有错误
|
||
/// [errorState] 错误状态组件
|
||
/// [child] 正常内容组件
|
||
class ErrorStateBuilder extends StatelessWidget {
|
||
/// 是否有错误
|
||
final bool hasError;
|
||
|
||
/// 错误状态组件
|
||
final Widget errorState;
|
||
|
||
/// 正常内容组件
|
||
final Widget child;
|
||
|
||
const ErrorStateBuilder({
|
||
super.key,
|
||
required this.hasError,
|
||
required this.errorState,
|
||
required this.child,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (hasError) {
|
||
return errorState;
|
||
}
|
||
return child;
|
||
}
|
||
}
|
||
|
||
/// 错误状态包装器 - 简化版本,自动构建错误状态
|
||
/// [hasError] 是否有错误
|
||
/// [errorType] 错误类型
|
||
/// [title] 错误标题
|
||
/// [message] 错误描述
|
||
/// [actionText] 操作按钮文本
|
||
/// [onAction] 操作按钮点击回调
|
||
/// [child] 正常内容组件
|
||
/// [showDetails] 是否显示详细错误信息
|
||
/// [details] 详细错误信息
|
||
class ErrorStateWrapper extends StatelessWidget {
|
||
/// 是否有错误
|
||
final bool hasError;
|
||
|
||
/// 错误类型
|
||
final ErrorType errorType;
|
||
|
||
/// 错误标题
|
||
final String title;
|
||
|
||
/// 错误描述
|
||
final String? message;
|
||
|
||
/// 操作按钮文本
|
||
final String? actionText;
|
||
|
||
/// 操作按钮点击回调
|
||
final VoidCallback? onAction;
|
||
|
||
/// 正常内容组件
|
||
final Widget child;
|
||
|
||
/// 自定义错误状态组件,优先级高于其他参数
|
||
final Widget? customErrorState;
|
||
|
||
/// 是否显示详细错误信息
|
||
final bool showDetails;
|
||
|
||
/// 详细错误信息
|
||
final String? details;
|
||
|
||
const ErrorStateWrapper({
|
||
super.key,
|
||
required this.hasError,
|
||
this.errorType = ErrorType.general,
|
||
required this.title,
|
||
required this.child,
|
||
this.message,
|
||
this.actionText,
|
||
this.onAction,
|
||
this.customErrorState,
|
||
this.showDetails = false,
|
||
this.details,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (hasError) {
|
||
return customErrorState ?? ErrorState(
|
||
errorType: errorType,
|
||
title: title,
|
||
message: message,
|
||
actionText: actionText,
|
||
onAction: onAction,
|
||
showDetails: showDetails,
|
||
details: details,
|
||
);
|
||
}
|
||
return child;
|
||
}
|
||
} |