541 lines
14 KiB
Dart
541 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// 空状态组件库 - 提供多种样式的空状态页面展示
|
||
/// 包括无数据、无结果、错误等不同场景的空状态显示
|
||
|
||
/// 基础空状态组件 - 通用的空状态显示组件
|
||
/// [icon] 空状态图标,使用Material Icons
|
||
/// [title] 空状态标题文本
|
||
/// [message] 空状态描述文本
|
||
/// [actionText] 操作按钮文本,null时不显示按钮
|
||
/// [onAction] 操作按钮点击回调,null时按钮不可点击
|
||
/// [iconSize] 图标大小,null时使用默认大小
|
||
/// [iconColor] 图标颜色,null时使用主题色彩
|
||
class EmptyState extends StatelessWidget {
|
||
/// 空状态图标,使用Material Icons
|
||
final IconData icon;
|
||
|
||
/// 空状态标题文本
|
||
final String title;
|
||
|
||
/// 空状态描述文本
|
||
final String? message;
|
||
|
||
/// 操作按钮文本,null时不显示按钮
|
||
final String? actionText;
|
||
|
||
/// 操作按钮点击回调,null时按钮不可点击
|
||
final VoidCallback? onAction;
|
||
|
||
/// 图标大小,null时使用默认大小
|
||
final double? iconSize;
|
||
|
||
/// 图标颜色,null时使用主题色彩
|
||
final Color? iconColor;
|
||
|
||
/// 自定义图标组件,优先级高于icon参数
|
||
final Widget? customIcon;
|
||
|
||
/// 自定义操作组件,优先级高于actionText参数
|
||
final Widget? customAction;
|
||
|
||
/// 主要内容垂直间距
|
||
final double spacing;
|
||
|
||
/// 主要内容内边距
|
||
final EdgeInsetsGeometry? padding;
|
||
|
||
/// 是否显示边框装饰
|
||
final bool showBorder;
|
||
|
||
/// 边框圆角半径
|
||
final double borderRadius;
|
||
|
||
/// 背景颜色,null时使用透明背景
|
||
final Color? backgroundColor;
|
||
|
||
const EmptyState({
|
||
super.key,
|
||
required this.icon,
|
||
required this.title,
|
||
this.message,
|
||
this.actionText,
|
||
this.onAction,
|
||
this.iconSize,
|
||
this.iconColor,
|
||
this.customIcon,
|
||
this.customAction,
|
||
this.spacing = 16,
|
||
this.padding,
|
||
this.showBorder = false,
|
||
this.borderRadius = 12,
|
||
this.backgroundColor,
|
||
});
|
||
|
||
/// 创建无数据空状态 - 用于列表、网格等无数据场景
|
||
factory EmptyState.noData({
|
||
String? title,
|
||
String? message,
|
||
String? actionText,
|
||
VoidCallback? onAction,
|
||
IconData? icon,
|
||
}) {
|
||
return EmptyState(
|
||
icon: icon ?? Icons.inbox_outlined,
|
||
title: title ?? '暂无数据',
|
||
message: message ?? '当前没有可显示的数据',
|
||
actionText: actionText,
|
||
onAction: onAction,
|
||
iconColor: Colors.grey,
|
||
);
|
||
}
|
||
|
||
/// 创建无搜索结果空状态 - 用于搜索无结果场景
|
||
factory EmptyState.noSearchResults({
|
||
String? searchQuery,
|
||
VoidCallback? onClearSearch,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.search_off_outlined,
|
||
title: '未找到相关结果',
|
||
message: searchQuery != null
|
||
? '未找到与"$searchQuery"相关的内容'
|
||
: '未找到相关内容,请尝试其他关键词',
|
||
actionText: onClearSearch != null ? '清除搜索' : null,
|
||
onAction: onClearSearch,
|
||
iconColor: Colors.grey,
|
||
);
|
||
}
|
||
|
||
/// 创建网络错误空状态 - 用于网络连接失败场景
|
||
factory EmptyState.networkError({
|
||
VoidCallback? onRetry,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.wifi_off_outlined,
|
||
title: '网络连接失败',
|
||
message: '请检查网络连接后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
iconColor: Colors.orange,
|
||
);
|
||
}
|
||
|
||
/// 创建服务器错误空状态 - 用于服务器异常场景
|
||
factory EmptyState.serverError({
|
||
VoidCallback? onRetry,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.cloud_off_outlined,
|
||
title: '服务器异常',
|
||
message: '服务器暂时无法访问,请稍后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
iconColor: Colors.red,
|
||
);
|
||
}
|
||
|
||
/// 创建权限不足空状态 - 用于无权限访问场景
|
||
factory EmptyState.permissionDenied({
|
||
VoidCallback? onRequestPermission,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.lock_outline,
|
||
title: '权限不足',
|
||
message: '您没有访问此内容的权限',
|
||
actionText: onRequestPermission != null ? '申请权限' : null,
|
||
onAction: onRequestPermission,
|
||
iconColor: Colors.orange,
|
||
);
|
||
}
|
||
|
||
/// 创建收藏为空状态 - 用于收藏列表为空场景
|
||
factory EmptyState.emptyFavorites({
|
||
VoidCallback? onBrowse,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.favorite_border_outlined,
|
||
title: '暂无收藏',
|
||
message: '您还没有收藏任何内容',
|
||
actionText: onBrowse != null ? '去浏览' : null,
|
||
onAction: onBrowse,
|
||
iconColor: Colors.pink,
|
||
);
|
||
}
|
||
|
||
/// 创建购物车为空状态 - 用于购物车为空场景
|
||
factory EmptyState.emptyCart({
|
||
VoidCallback? onShop,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.shopping_cart_outlined,
|
||
title: '购物车是空的',
|
||
message: '您还没有添加任何商品',
|
||
actionText: onShop != null ? '去购物' : null,
|
||
onAction: onShop,
|
||
iconColor: Colors.blue,
|
||
);
|
||
}
|
||
|
||
/// 创建历史记录为空状态 - 用于历史记录为空场景
|
||
factory EmptyState.emptyHistory({
|
||
VoidCallback? onBrowse,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.history_outlined,
|
||
title: '暂无历史记录',
|
||
message: '您还没有任何操作记录',
|
||
actionText: onBrowse != null ? '去浏览' : null,
|
||
onAction: onBrowse,
|
||
iconColor: Colors.grey,
|
||
);
|
||
}
|
||
|
||
/// 创建文件为空状态 - 用于文件列表为空场景
|
||
factory EmptyState.emptyFiles({
|
||
VoidCallback? onUpload,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.folder_open_outlined,
|
||
title: '文件夹是空的',
|
||
message: '此文件夹中还没有任何文件',
|
||
actionText: onUpload != null ? '上传文件' : null,
|
||
onAction: onUpload,
|
||
iconColor: Colors.blue,
|
||
);
|
||
}
|
||
|
||
/// 创建图片为空状态 - 用于图片列表为空场景
|
||
factory EmptyState.emptyImages({
|
||
VoidCallback? onAddImages,
|
||
}) {
|
||
return EmptyState(
|
||
icon: Icons.photo_library_outlined,
|
||
title: '暂无图片',
|
||
message: '您还没有添加任何图片',
|
||
actionText: onAddImages != null ? '添加图片' : null,
|
||
onAction: onAddImages,
|
||
iconColor: Colors.purple,
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
Widget content = Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
// 图标部分
|
||
if (customIcon != null) ...[
|
||
customIcon!,
|
||
SizedBox(height: spacing),
|
||
] else ...[
|
||
Icon(
|
||
icon,
|
||
size: iconSize ?? 64,
|
||
color: iconColor ?? theme.colorScheme.onSurface.withOpacity(0.4),
|
||
),
|
||
SizedBox(height: spacing),
|
||
],
|
||
|
||
// 标题部分
|
||
Text(
|
||
title,
|
||
style: theme.textTheme.titleLarge?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
color: theme.colorScheme.onSurface.withOpacity(0.8),
|
||
),
|
||
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 (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(
|
||
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 ?? theme.colorScheme.surface.withOpacity(0.5),
|
||
border: Border.all(
|
||
color: theme.colorScheme.outline.withOpacity(0.2),
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// 空状态页面 - 全屏空状态显示,包含图标、文本和操作按钮
|
||
/// [icon] 空状态图标
|
||
/// [title] 空状态标题
|
||
/// [message] 空状态描述文本
|
||
/// [actionText] 操作按钮文本
|
||
/// [onAction] 操作按钮点击回调
|
||
/// [showAppBar] 是否显示应用栏
|
||
/// [appBarTitle] 应用栏标题
|
||
class EmptyStatePage extends StatelessWidget {
|
||
/// 空状态图标
|
||
final IconData icon;
|
||
|
||
/// 空状态标题
|
||
final String title;
|
||
|
||
/// 空状态描述文本
|
||
final String? message;
|
||
|
||
/// 操作按钮文本
|
||
final String? actionText;
|
||
|
||
/// 操作按钮点击回调
|
||
final VoidCallback? onAction;
|
||
|
||
/// 是否显示应用栏
|
||
final bool showAppBar;
|
||
|
||
/// 应用栏标题
|
||
final String? appBarTitle;
|
||
|
||
/// 自定义空状态组件,优先级高于其他参数
|
||
final Widget? customEmptyState;
|
||
|
||
/// 背景颜色,null时使用主题背景色
|
||
final Color? backgroundColor;
|
||
|
||
const EmptyStatePage({
|
||
super.key,
|
||
required this.icon,
|
||
required this.title,
|
||
this.message,
|
||
this.actionText,
|
||
this.onAction,
|
||
this.showAppBar = false,
|
||
this.appBarTitle,
|
||
this.customEmptyState,
|
||
this.backgroundColor,
|
||
});
|
||
|
||
/// 创建无数据空状态页面
|
||
factory EmptyStatePage.noData({
|
||
String? title,
|
||
String? message,
|
||
String? actionText,
|
||
VoidCallback? onAction,
|
||
bool showAppBar = false,
|
||
String? appBarTitle,
|
||
}) {
|
||
return EmptyStatePage(
|
||
icon: Icons.inbox_outlined,
|
||
title: title ?? '暂无数据',
|
||
message: message ?? '当前没有可显示的数据',
|
||
actionText: actionText,
|
||
onAction: onAction,
|
||
showAppBar: showAppBar,
|
||
appBarTitle: appBarTitle ?? '无数据',
|
||
);
|
||
}
|
||
|
||
/// 创建无搜索结果空状态页面
|
||
factory EmptyStatePage.noSearchResults({
|
||
String? searchQuery,
|
||
VoidCallback? onClearSearch,
|
||
bool showAppBar = false,
|
||
}) {
|
||
return EmptyStatePage(
|
||
icon: Icons.search_off_outlined,
|
||
title: '未找到相关结果',
|
||
message: searchQuery != null
|
||
? '未找到与"$searchQuery"相关的内容'
|
||
: '未找到相关内容,请尝试其他关键词',
|
||
actionText: onClearSearch != null ? '清除搜索' : null,
|
||
onAction: onClearSearch,
|
||
showAppBar: showAppBar,
|
||
appBarTitle: '搜索结果',
|
||
);
|
||
}
|
||
|
||
/// 创建网络错误空状态页面
|
||
factory EmptyStatePage.networkError({
|
||
VoidCallback? onRetry,
|
||
bool showAppBar = false,
|
||
}) {
|
||
return EmptyStatePage(
|
||
icon: Icons.wifi_off_outlined,
|
||
title: '网络连接失败',
|
||
message: '请检查网络连接后重试',
|
||
actionText: '重新加载',
|
||
onAction: onRetry,
|
||
showAppBar: showAppBar,
|
||
appBarTitle: '网络错误',
|
||
);
|
||
}
|
||
|
||
@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: customEmptyState ?? EmptyState(
|
||
icon: icon,
|
||
title: title,
|
||
message: message,
|
||
actionText: actionText,
|
||
onAction: onAction,
|
||
showBorder: false,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 空状态构建器 - 根据条件显示空状态或正常内容
|
||
/// [isEmpty] 是否为空状态
|
||
/// [emptyState] 空状态组件
|
||
/// [child] 正常内容组件
|
||
class EmptyStateBuilder extends StatelessWidget {
|
||
/// 是否为空状态
|
||
final bool isEmpty;
|
||
|
||
/// 空状态组件
|
||
final Widget emptyState;
|
||
|
||
/// 正常内容组件
|
||
final Widget child;
|
||
|
||
const EmptyStateBuilder({
|
||
super.key,
|
||
required this.isEmpty,
|
||
required this.emptyState,
|
||
required this.child,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (isEmpty) {
|
||
return emptyState;
|
||
}
|
||
return child;
|
||
}
|
||
}
|
||
|
||
/// 空状态包装器 - 简化版本,自动构建空状态
|
||
/// [isEmpty] 是否为空状态
|
||
/// [icon] 空状态图标
|
||
/// [title] 空状态标题
|
||
/// [message] 空状态描述
|
||
/// [actionText] 操作按钮文本
|
||
/// [onAction] 操作按钮点击回调
|
||
/// [child] 正常内容组件
|
||
class EmptyStateWrapper extends StatelessWidget {
|
||
/// 是否为空状态
|
||
final bool isEmpty;
|
||
|
||
/// 空状态图标
|
||
final IconData icon;
|
||
|
||
/// 空状态标题
|
||
final String title;
|
||
|
||
/// 空状态描述
|
||
final String? message;
|
||
|
||
/// 操作按钮文本
|
||
final String? actionText;
|
||
|
||
/// 操作按钮点击回调
|
||
final VoidCallback? onAction;
|
||
|
||
/// 正常内容组件
|
||
final Widget child;
|
||
|
||
/// 自定义空状态组件,优先级高于其他参数
|
||
final Widget? customEmptyState;
|
||
|
||
const EmptyStateWrapper({
|
||
super.key,
|
||
required this.isEmpty,
|
||
required this.icon,
|
||
required this.title,
|
||
required this.child,
|
||
this.message,
|
||
this.actionText,
|
||
this.onAction,
|
||
this.customEmptyState,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (isEmpty) {
|
||
return customEmptyState ?? EmptyState(
|
||
icon: icon,
|
||
title: title,
|
||
message: message,
|
||
actionText: actionText,
|
||
onAction: onAction,
|
||
);
|
||
}
|
||
return child;
|
||
}
|
||
} |