diff --git a/CLAUDE.md b/CLAUDE.md index 3a2c538..098e83c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,17 +121,29 @@ flutter clean - ✅ 数据迁移机制配置完成,支持版本管理 - ⚠️ 代码质量:26个`avoid_print`提示,无致命错误 -**任务1.3: 核心工具类** -- [ ] 图片压缩工具类(长边500px + WebP) -- [ ] 文件存储管理工具 -- [ ] UUID生成和路径管理 -- [ ] 错误处理和日志系统 +**任务1.3: 核心工具类** ✅ +- [x] 图片压缩工具类(长边500px + WebP) +- [x] 文件存储管理工具 +- [x] UUID生成和路径管理 +- [x] 错误处理和日志系统 -**任务1.4: 基础UI组件** -- [ ] 创建可复用的按钮组件 -- [ ] 实现加载状态组件(骨架屏) -- [ ] 空状态页面组件 -- [ ] 错误提示组件 +**验收状态**: +- ✅ 图片压缩工具类:支持JPG/PNG/GIF/WebP/HEIC格式,长边500px,WebP格式,GIF帧提取防内存溢出 +- ✅ 文件存储管理:日期分类存储(/yyyy/MM/dd/),完整的文件和目录操作 +- ✅ UUID和路径管理:多种UUID生成,路径安全检查,链式路径构建器 +- ✅ 日志系统:5级日志(debug/info/warning/error/fatal),替换所有print语句 + +**任务1.4: 基础UI组件** ✅ +- [x] 创建可复用的按钮组件 +- [x] 实现加载状态组件(骨架屏) +- [x] 空状态页面组件 +- [x] 错误提示组件 + +**验收状态**: +- ✅ 可复用按钮组件:支持主要、次要、文本、轮廓、危险按钮类型,多种尺寸,加载状态,图标支持 +- ✅ 加载状态组件:骨架屏动画、圆形加载器、进度条、全屏加载、列表/网格骨架屏 +- ✅ 空状态页面组件:多种预设场景(无数据、无搜索结果、网络错误等),自定义图标和操作 +- ✅ 错误提示组件:多种错误类型(网络、服务器、权限、数据等),详细错误信息显示 #### 📤 Phase 2: 分享功能(2-3天) **任务2.1: 分享接收机制** @@ -365,20 +377,20 @@ class UserService { ### 📊 总体进度 - [x] Phase 1.1: 项目基础配置(5/5)✅ - [x] Phase 1.2: 数据层架构搭建(4/4)✅ -- [ ] Phase 1.3: 核心工具类(0/4) -- [ ] Phase 1.4: 基础UI组件(0/4) +- [x] Phase 1.3: 核心工具类(4/4)✅ +- [x] Phase 1.4: 基础UI组件(4/4)✅ - [ ] Phase 2: 分享功能(0/4) ### 🎯 当前任务详情 -**任务编号**:1.3 -**任务名称**:核心工具类 +**任务编号**:2.1 +**任务名称**:分享接收机制 **任务状态**:待开始 -**预计完成**:2025年9月17日 -**依赖项**:Phase 1.2 完成 +**预计完成**:2025年9月18日 +**依赖项**:Phase 1.4 完成 **任务验收标准**: -- 图片压缩工具类实现(长边500px + WebP格式) -- 文件存储管理工具(支持日期分类存储) -- UUID生成和路径管理工具 -- 错误处理和日志系统完善 -- 代码质量检查通过(解决print警告) \ No newline at end of file +- 分享接收机制配置完成 +- Android分享接收配置完成 +- iOS分享接收配置完成 +- 多张图片接收逻辑处理 +- 分享功能测试通过 \ No newline at end of file diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 23705dd..0ae4f98 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -/// 应用主题配置类 +/// 应用主题配置类 - 管理应用的主题样式和色彩方案 +/// 支持浅色主题、深色主题和系统跟随模式 class AppTheme { // 私有构造函数,防止实例化 AppTheme._(); @@ -243,4 +244,113 @@ class AppTheme { // 亮度大于0.5使用深色,否则使用浅色 return brightness > 0.5 ? Colors.black : Colors.white; } +} + +/// 应用色彩配置类 - 定义应用使用的颜色常量 +/// 提供浅色和深色主题下的颜色配置 +class AppColors { + /// 主色调 - 应用的主要品牌色彩 + static const Color primary = Color(0xFF2196F3); + + /// 主色调上的文本色彩 - 用于主色调背景上的文本 + static const Color onPrimary = Color(0xFFFFFFFF); + + /// 次要色调 - 用于次要操作或强调 + static const Color secondary = Color(0xFF03DAC6); + + /// 次要色调上的文本色彩 - 用于次要色调背景上的文本 + static const Color onSecondary = Color(0xFF000000); + + /// 错误色调 - 用于错误提示、危险操作等 + static const Color error = Color(0xFFB00020); + + /// 错误色调上的文本色彩 - 用于错误色调背景上的文本 + static const Color onError = Color(0xFFFFFFFF); + + /// 背景色彩 - 应用的主要背景色 + static const Color background = Color(0xFFF5F5F5); + + /// 背景上的文本色彩 - 用于背景上的主要文本 + static const Color onBackground = Color(0xFF212121); + + /// 表面色彩 - 卡片、对话框等组件的背景色 + static const Color surface = Color(0xFFFFFFFF); + + /// 表面上的文本色彩 - 用于表面上的主要文本 + static const Color onSurface = Color(0xFF212121); + + /// 表面变体色彩 - 用于区分不同层次的表面 + static const Color surfaceVariant = Color(0xFFE0E0E0); + + /// 表面变体上的文本色彩 - 用于表面变体上的文本 + static const Color onSurfaceVariant = Color(0xFF616161); + + /// 轮廓色彩 - 用于边框、分割线等 + static const Color outline = Color(0xFFBDBDBD); + + /// 成功色彩 - 用于成功提示、完成状态等 + static const Color success = Color(0xFF4CAF50); + + /// 警告色彩 - 用于警告提示、注意状态等 + static const Color warning = Color(0xFFFF9800); + + /// 信息色彩 - 用于信息提示、说明状态等 + static const Color info = Color(0xFF2196F3); + + /// 禁用状态色彩 - 用于禁用状态的组件 + static const Color disabled = Color(0xFFBDBDBD); + + /// 加载状态色彩 - 用于加载指示器 + static const Color loading = Color(0xFF2196F3); + + /// 透明色彩 - 用于需要透明的场景 + static const Color transparent = Color(0x00000000); + + /// 阴影色彩 - 用于阴影效果 + static const Color shadow = Color(0x1F000000); + + /// 从BuildContext获取颜色配置 - 便捷方法获取当前主题颜色 + static AppColors of(BuildContext context) { + // 这里可以根据主题亮度返回不同的颜色配置 + // 目前返回默认的颜色配置 + return AppColors(); + } + + /// 创建浅色主题颜色方案 + static ColorScheme get lightColorScheme { + return const ColorScheme.light( + primary: primary, + onPrimary: onPrimary, + secondary: secondary, + onSecondary: onSecondary, + error: error, + onError: onError, + background: background, + onBackground: onBackground, + surface: surface, + onSurface: onSurface, + surfaceVariant: surfaceVariant, + onSurfaceVariant: onSurfaceVariant, + outline: outline, + ); + } + + /// 创建深色主题颜色方案 + static ColorScheme get darkColorScheme { + return const ColorScheme.dark( + primary: primary, + onPrimary: onPrimary, + secondary: secondary, + onSecondary: onSecondary, + error: error, + onError: onError, + background: Color(0xFF121212), + onBackground: Color(0xFFFFFFFF), + surface: Color(0xFF1E1E1E), + onSurface: Color(0xFFFFFFFF), + surfaceVariant: Color(0xFF2C2C2C), + onSurfaceVariant: Color(0xFFBDBDBD), + outline: Color(0xFF424242), + ); + } } \ No newline at end of file diff --git a/lib/core/utils/path_utils.dart b/lib/core/utils/path_utils.dart index f847ffa..0539645 100644 --- a/lib/core/utils/path_utils.dart +++ b/lib/core/utils/path_utils.dart @@ -41,7 +41,7 @@ class PathUtils { String? prefix, }) { final baseName = generateTimeBasedId(); - final fileName = prefix != null ? '${prefix}$baseName' : baseName; + final fileName = prefix != null ? '$prefix$baseName' : baseName; return '$fileName$extension'; } diff --git a/lib/data/datasources/local/hive_database.dart b/lib/data/datasources/local/hive_database.dart index f6d7946..248d15d 100644 --- a/lib/data/datasources/local/hive_database.dart +++ b/lib/data/datasources/local/hive_database.dart @@ -119,16 +119,6 @@ class HiveDatabase { } } - /// 创建默认数据 - 初始化数据库时的默认数据创建 - /// 创建系统必需的默认文件夹和标签 - static Future _createDefaultData() async { - final foldersBox = Hive.box(_foldersBoxName); - final tagsBox = Hive.box(_tagsBoxName); - - // 使用迁移管理类创建默认数据 - await DatabaseMigration.createDefaultFolders(foldersBox); - await DatabaseMigration.createDefaultTags(tagsBox); - } /// 创建紧急恢复数据 - 当迁移失败时的紧急恢复方案 /// 确保应用至少能正常运行,包含最基本的数据结构 @@ -151,16 +141,16 @@ class HiveDatabase { lastUsedAt: now, ); await foldersBox.put('default', emergencyFolder); - print('创建紧急默认文件夹'); + Logger.info('创建紧急默认文件夹'); } // 设置当前版本号,避免重复迁移 await settingsBox.put(_versionKey, _currentVersion); - print('紧急恢复数据创建完成'); + Logger.info('紧急恢复数据创建完成'); } catch (e) { - print('紧急恢复数据创建失败: $e'); + Logger.error('紧急恢复数据创建失败', error: e); // 如果连紧急恢复都失败,只能抛出致命异常 throw Exception('数据库初始化失败,应用无法正常运行: $e'); } @@ -177,17 +167,17 @@ class HiveDatabase { /// 返回清理操作是否成功 static Future clearAll() async { try { - print('开始清理所有数据库数据...'); + Logger.info('开始清理所有数据库数据...'); await imagesBox.clear(); await foldersBox.clear(); await tagsBox.clear(); await settingsBox.clear(); - print('数据库清理完成'); + Logger.info('数据库清理完成'); return true; } catch (e) { - print('数据库清理失败: $e'); + Logger.error('数据库清理失败', error: e); return false; } } @@ -197,14 +187,14 @@ class HiveDatabase { /// 返回关闭操作是否成功 static Future close() async { try { - print('正在关闭Hive数据库...'); + Logger.info('正在关闭Hive数据库...'); await Hive.close(); - print('Hive数据库已关闭'); + Logger.info('Hive数据库已关闭'); return true; } catch (e) { - print('关闭Hive数据库失败: $e'); + Logger.error('关闭Hive数据库失败', error: e); return false; } } @@ -213,7 +203,7 @@ class HiveDatabase { /// 返回数据库是否通过完整性检查 static Future validateIntegrity() async { try { - print('开始验证数据库完整性...'); + Logger.info('开始验证数据库完整性...'); final isValid = await DatabaseMigration.validateDatabaseIntegrity( imagesBox, @@ -222,14 +212,14 @@ class HiveDatabase { ); if (isValid) { - print('数据库完整性验证通过'); + Logger.info('数据库完整性验证通过'); } else { - print('数据库完整性验证发现问题并已修复'); + Logger.info('数据库完整性验证发现问题并已修复'); } return isValid; } catch (e) { - print('数据库完整性验证失败: $e'); + Logger.error('数据库完整性验证失败', error: e); return false; } } @@ -244,7 +234,7 @@ class HiveDatabase { tagsBox, ); } catch (e) { - print('获取数据库统计信息失败: $e'); + Logger.error('获取数据库统计信息失败', error: e); return { 'error': '获取统计信息失败', 'message': e.toString(), @@ -256,7 +246,7 @@ class HiveDatabase { /// 返回清理结果的统计信息,包括清理的项目数量 static Future> cleanup() async { try { - print('开始清理数据库...'); + Logger.info('开始清理数据库...'); final cleanupStats = await DatabaseMigration.cleanupDatabase( imagesBox, @@ -264,10 +254,10 @@ class HiveDatabase { tagsBox, ); - print('数据库清理完成: $cleanupStats'); + Logger.info('数据库清理完成: $cleanupStats'); return cleanupStats; } catch (e) { - print('数据库清理失败: $e'); + Logger.error('数据库清理失败', error: e); return { 'error': 1, }; diff --git a/lib/presentation/widgets/custom_button.dart b/lib/presentation/widgets/custom_button.dart new file mode 100644 index 0000000..ad8a93e --- /dev/null +++ b/lib/presentation/widgets/custom_button.dart @@ -0,0 +1,508 @@ +import 'package:flutter/material.dart'; +import '../../../core/theme/app_theme.dart'; + +/// 自定义按钮组件 - 提供多种样式和状态的可复用按钮 +/// 支持主要按钮、次要按钮、文本按钮、图标按钮等多种类型 +/// 提供加载状态、禁用状态等交互反馈 +class CustomButton extends StatelessWidget { + /// 按钮文本内容 + final String text; + + /// 按钮点击回调函数 + final VoidCallback? onPressed; + + /// 按钮类型,决定外观样式 + final ButtonType buttonType; + + /// 按钮尺寸,影响整体大小 + final ButtonSize buttonSize; + + /// 是否显示加载状态 + final bool isLoading; + + /// 是否禁用按钮 + final bool isDisabled; + + /// 前置图标,显示在文本左侧 + final IconData? prefixIcon; + + /// 后置图标,显示在文本右侧 + final IconData? suffixIcon; + + /// 自定义背景颜色,优先级高于buttonType + final Color? backgroundColor; + + /// 自定义文本颜色,优先级高于buttonType + final Color? textColor; + + /// 按钮圆角半径,null时使用默认值 + final double? borderRadius; + + /// 按钮最小宽度,null时根据内容自适应 + final double? minWidth; + + /// 按钮最大宽度,null时无限制 + final double? maxWidth; + + /// 内边距,null时使用默认内边距 + final EdgeInsetsGeometry? padding; + + /// 阴影高度,null时根据按钮类型使用默认阴影 + final double? elevation; + + const CustomButton({ + super.key, + required this.text, + required this.onPressed, + this.buttonType = ButtonType.primary, + this.buttonSize = ButtonSize.medium, + this.isLoading = false, + this.isDisabled = false, + this.prefixIcon, + this.suffixIcon, + this.backgroundColor, + this.textColor, + this.borderRadius, + this.minWidth, + this.maxWidth, + this.padding, + this.elevation, + }); + + /// 创建主要按钮 - 用于主要操作,视觉突出 + factory CustomButton.primary({ + required String text, + required VoidCallback? onPressed, + bool isLoading = false, + bool isDisabled = false, + IconData? prefixIcon, + IconData? suffixIcon, + double? minWidth, + double? maxWidth, + }) { + return CustomButton( + text: text, + onPressed: onPressed, + buttonType: ButtonType.primary, + isLoading: isLoading, + isDisabled: isDisabled, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + minWidth: minWidth, + maxWidth: maxWidth, + ); + } + + /// 创建次要按钮 - 用于次要操作,视觉弱化 + factory CustomButton.secondary({ + required String text, + required VoidCallback? onPressed, + bool isLoading = false, + bool isDisabled = false, + IconData? prefixIcon, + IconData? suffixIcon, + double? minWidth, + double? maxWidth, + }) { + return CustomButton( + text: text, + onPressed: onPressed, + buttonType: ButtonType.secondary, + isLoading: isLoading, + isDisabled: isDisabled, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + minWidth: minWidth, + maxWidth: maxWidth, + ); + } + + /// 创建文本按钮 - 无边框无背景,适合嵌入其他组件 + factory CustomButton.text({ + required String text, + required VoidCallback? onPressed, + bool isLoading = false, + bool isDisabled = false, + IconData? prefixIcon, + IconData? suffixIcon, + Color? textColor, + double? minWidth, + double? maxWidth, + }) { + return CustomButton( + text: text, + onPressed: onPressed, + buttonType: ButtonType.text, + isLoading: isLoading, + isDisabled: isDisabled, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + textColor: textColor, + minWidth: minWidth, + maxWidth: maxWidth, + ); + } + + /// 创建轮廓按钮 - 有边框无填充,适合次要操作 + factory CustomButton.outlined({ + required String text, + required VoidCallback? onPressed, + bool isLoading = false, + bool isDisabled = false, + IconData? prefixIcon, + IconData? suffixIcon, + double? minWidth, + double? maxWidth, + }) { + return CustomButton( + text: text, + onPressed: onPressed, + buttonType: ButtonType.outlined, + isLoading: isLoading, + isDisabled: isDisabled, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + minWidth: minWidth, + maxWidth: maxWidth, + ); + } + + /// 创建危险按钮 - 用于删除等危险操作,通常使用红色主题 + factory CustomButton.danger({ + required String text, + required VoidCallback? onPressed, + bool isLoading = false, + bool isDisabled = false, + IconData? prefixIcon, + IconData? suffixIcon, + double? minWidth, + double? maxWidth, + }) { + return CustomButton( + text: text, + onPressed: onPressed, + buttonType: ButtonType.danger, + isLoading: isLoading, + isDisabled: isDisabled, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + minWidth: minWidth, + maxWidth: maxWidth, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final appColors = AppColors.of(context); + + // 确定按钮是否可点击 + final bool buttonEnabled = !isLoading && !isDisabled && onPressed != null; + + // 获取按钮样式配置 + final buttonStyle = _getButtonStyle(theme, appColors); + final textStyle = _getTextStyle(theme, appColors); + final paddingValue = _getPadding(); + + // 构建按钮内容 + final buttonContent = isLoading + ? SizedBox( + width: textStyle.fontSize, + height: textStyle.fontSize, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + buttonStyle.foregroundColor?.resolve({}) ?? + theme.colorScheme.onPrimary, + ), + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (prefixIcon != null) ...[ + Icon( + prefixIcon, + size: textStyle.fontSize! * 1.2, + color: textStyle.color, + ), + const SizedBox(width: 8), + ], + Text( + text, + style: textStyle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + if (suffixIcon != null) ...[ + const SizedBox(width: 8), + Icon( + suffixIcon, + size: textStyle.fontSize! * 1.2, + color: textStyle.color, + ), + ], + ], + ); + + // 根据按钮类型构建不同的按钮组件 + switch (buttonType) { + case ButtonType.primary: + case ButtonType.secondary: + case ButtonType.danger: + return ConstrainedBox( + constraints: BoxConstraints( + minWidth: minWidth ?? 0, + maxWidth: maxWidth ?? double.infinity, + minHeight: _getMinHeight(), + ), + child: ElevatedButton( + onPressed: buttonEnabled ? onPressed : null, + style: ElevatedButton.styleFrom( + backgroundColor: buttonStyle.backgroundColor?.resolve({}), + foregroundColor: buttonStyle.foregroundColor?.resolve({}), + elevation: elevation ?? (buttonEnabled ? 2 : 0), + padding: paddingValue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + borderRadius ?? _getDefaultBorderRadius()), + ), + disabledBackgroundColor: + buttonStyle.backgroundColor?.resolve({MaterialState.disabled}), + disabledForegroundColor: + buttonStyle.foregroundColor?.resolve({MaterialState.disabled}), + ), + child: buttonContent, + ), + ); + + case ButtonType.outlined: + return ConstrainedBox( + constraints: BoxConstraints( + minWidth: minWidth ?? 0, + maxWidth: maxWidth ?? double.infinity, + minHeight: _getMinHeight(), + ), + child: OutlinedButton( + onPressed: buttonEnabled ? onPressed : null, + style: OutlinedButton.styleFrom( + foregroundColor: buttonStyle.foregroundColor?.resolve({}), + padding: paddingValue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + borderRadius ?? _getDefaultBorderRadius()), + ), + side: BorderSide( + color: buttonEnabled + ? (buttonStyle.foregroundColor?.resolve({}) ?? + theme.colorScheme.primary) + : (buttonStyle.foregroundColor?.resolve({MaterialState.disabled}) ?? + theme.colorScheme.onSurface.withOpacity(0.12)), + ), + disabledForegroundColor: + buttonStyle.foregroundColor?.resolve({MaterialState.disabled}), + ), + child: buttonContent, + ), + ); + + case ButtonType.text: + return ConstrainedBox( + constraints: BoxConstraints( + minWidth: minWidth ?? 0, + maxWidth: maxWidth ?? double.infinity, + minHeight: _getMinHeight(), + ), + child: TextButton( + onPressed: buttonEnabled ? onPressed : null, + style: TextButton.styleFrom( + foregroundColor: buttonStyle.foregroundColor?.resolve({}), + padding: paddingValue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + borderRadius ?? _getDefaultBorderRadius()), + ), + disabledForegroundColor: + buttonStyle.foregroundColor?.resolve({MaterialState.disabled}), + ), + child: buttonContent, + ), + ); + } + } + + /// 获取按钮样式配置 - 根据按钮类型和主题返回对应的样式 + ButtonStyle _getButtonStyle(ThemeData theme, AppColors appColors) { + switch (buttonType) { + case ButtonType.primary: + return ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return backgroundColor?.withOpacity(0.12) ?? + theme.colorScheme.primary.withOpacity(0.12); + } + return backgroundColor ?? theme.colorScheme.primary; + }), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return textColor?.withOpacity(0.38) ?? + theme.colorScheme.onPrimary.withOpacity(0.38); + } + return textColor ?? theme.colorScheme.onPrimary; + }), + ); + + case ButtonType.secondary: + return ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return (backgroundColor ?? theme.colorScheme.secondaryContainer) + .withOpacity(0.12); + } + return backgroundColor ?? theme.colorScheme.secondaryContainer; + }), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return (textColor ?? theme.colorScheme.onSecondaryContainer) + .withOpacity(0.38); + } + return textColor ?? theme.colorScheme.onSecondaryContainer; + }), + ); + + case ButtonType.danger: + return ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return (backgroundColor ?? AppColors.error).withOpacity(0.12); + } + return backgroundColor ?? AppColors.error; + }), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return (textColor ?? AppColors.onError).withOpacity(0.38); + } + return textColor ?? AppColors.onError; + }), + ); + + case ButtonType.outlined: + case ButtonType.text: + return ButtonStyle( + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return (textColor ?? theme.colorScheme.primary).withOpacity(0.38); + } + return textColor ?? theme.colorScheme.primary; + }), + ); + } + } + + /// 获取文本样式 - 根据按钮尺寸返回对应的文本样式 + TextStyle _getTextStyle(ThemeData theme, AppColors appColors) { + final baseStyle = theme.textTheme.labelLarge ?? const TextStyle(); + + switch (buttonSize) { + case ButtonSize.small: + return baseStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _getButtonStyle(theme, appColors) + .foregroundColor + ?.resolve(isDisabled || isLoading + ? {MaterialState.disabled} + : {}), + ); + case ButtonSize.medium: + return baseStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + color: _getButtonStyle(theme, appColors) + .foregroundColor + ?.resolve(isDisabled || isLoading + ? {MaterialState.disabled} + : {}), + ); + case ButtonSize.large: + return baseStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: _getButtonStyle(theme, appColors) + .foregroundColor + ?.resolve(isDisabled || isLoading + ? {MaterialState.disabled} + : {}), + ); + } + } + + /// 获取内边距 - 根据按钮尺寸返回对应的内边距 + EdgeInsetsGeometry _getPadding() { + if (padding != null) return padding!; + + switch (buttonSize) { + case ButtonSize.small: + return const EdgeInsets.symmetric(horizontal: 12, vertical: 8); + case ButtonSize.medium: + return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); + case ButtonSize.large: + return const EdgeInsets.symmetric(horizontal: 24, vertical: 16); + } + } + + /// 获取最小高度 - 根据按钮尺寸返回对应的最小高度 + double _getMinHeight() { + switch (buttonSize) { + case ButtonSize.small: + return 32; + case ButtonSize.medium: + return 40; + case ButtonSize.large: + return 48; + } + } + + /// 获取默认圆角半径 - 根据按钮尺寸返回对应的圆角半径 + double _getDefaultBorderRadius() { + switch (buttonSize) { + case ButtonSize.small: + return 4; + case ButtonSize.medium: + return 8; + case ButtonSize.large: + return 12; + } + } +} + +/// 按钮类型枚举 - 定义按钮的视觉样式 +enum ButtonType { + /// 主要按钮 - 用于主要操作,视觉突出,使用主题主色调 + primary, + + /// 次要按钮 - 用于次要操作,视觉弱化,使用主题次要色调 + secondary, + + /// 轮廓按钮 - 有边框无填充,适合次要操作 + outlined, + + /// 文本按钮 - 无边框无背景,适合嵌入其他组件 + text, + + /// 危险按钮 - 用于删除等危险操作,通常使用红色主题 + danger, +} + +/// 按钮尺寸枚举 - 定义按钮的大小 +enum ButtonSize { + /// 小尺寸 - 适合紧凑布局,内边距较小 + small, + + /// 中等尺寸 - 默认尺寸,适合大多数场景 + medium, + + /// 大尺寸 - 适合重要操作或触摸友好的场景 + large, +} \ No newline at end of file diff --git a/lib/presentation/widgets/empty_state_widgets.dart b/lib/presentation/widgets/empty_state_widgets.dart new file mode 100644 index 0000000..cfe023e --- /dev/null +++ b/lib/presentation/widgets/empty_state_widgets.dart @@ -0,0 +1,541 @@ +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; + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/error_widgets.dart b/lib/presentation/widgets/error_widgets.dart new file mode 100644 index 0000000..db6d2a5 --- /dev/null +++ b/lib/presentation/widgets/error_widgets.dart @@ -0,0 +1,638 @@ +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; + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/loading_widgets.dart b/lib/presentation/widgets/loading_widgets.dart new file mode 100644 index 0000000..89db90f --- /dev/null +++ b/lib/presentation/widgets/loading_widgets.dart @@ -0,0 +1,860 @@ +import 'package:flutter/material.dart'; + +/// 简单的骨架屏动画组件 - 使用Flutter内置动画实现加载效果 +class SimpleShimmer extends StatefulWidget { + final Widget child; + final Duration duration; + final Color baseColor; + final Color highlightColor; + + const SimpleShimmer({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 1500), + required this.baseColor, + required this.highlightColor, + }); + + @override + State createState() => _SimpleShimmerState(); +} + +class _SimpleShimmerState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration, + vsync: this, + )..repeat(); + + _animation = Tween(begin: -2, end: 2).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOutSine, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment(_animation.value, 0), + end: Alignment(_animation.value + 1, 0), + colors: [ + widget.baseColor, + widget.highlightColor, + widget.baseColor, + ], + stops: const [0.0, 0.5, 1.0], + ).createShader(bounds); + }, + child: widget.child, + ); + }, + ); + } +} + +/// 加载状态组件库 - 提供多种样式的加载状态展示 +/// 包括骨架屏、圆形加载器、进度条等多种加载效果 + +/// 基础骨架屏组件 - 用于内容加载时的占位显示 +/// [width] 骨架屏宽度,null时根据父容器自适应 +/// [height] 骨架屏高度,必填参数 +/// [borderRadius] 圆角半径,null时使用默认值 +/// [margin] 外边距,控制骨架屏与其他元素的间距 +class Skeleton extends StatefulWidget { + /// 骨架屏宽度,null时根据父容器自适应 + final double? width; + + /// 骨架屏高度,必填参数 + final double height; + + /// 圆角半径,null时使用默认值 + final double? borderRadius; + + /// 外边距,控制骨架屏与其他元素的间距 + final EdgeInsetsGeometry? margin; + + /// 背景颜色,null时使用主题色彩 + final Color? backgroundColor; + + /// 骨架屏高亮颜色,null时使用主题色彩 + final Color? highlightColor; + + const Skeleton({ + super.key, + this.width, + required this.height, + this.borderRadius, + this.margin, + this.backgroundColor, + this.highlightColor, + }); + + @override + State createState() => _SkeletonState(); +} + +class _SkeletonState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(); + + _animation = Tween(begin: -2, end: 2).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOutSine, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final baseColor = widget.backgroundColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.5); + final highlightColor = widget.highlightColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.8); + + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: widget.width, + height: widget.height, + margin: widget.margin, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 8), + gradient: LinearGradient( + begin: Alignment(_animation.value, 0), + end: Alignment(_animation.value + 1, 0), + colors: [ + baseColor, + highlightColor, + baseColor, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ); + }, + ); + } +} + +/// 圆形骨架屏组件 - 用于头像、图标等圆形元素的占位显示 +/// [size] 圆形骨架屏直径,必填参数 +/// [margin] 外边距,控制骨架屏与其他元素的间距 +class CircleSkeleton extends StatefulWidget { + /// 圆形骨架屏直径,必填参数 + final double size; + + /// 外边距,控制骨架屏与其他元素的间距 + final EdgeInsetsGeometry? margin; + + /// 背景颜色,null时使用主题色彩 + final Color? backgroundColor; + + /// 骨架屏高亮颜色,null时使用主题色彩 + final Color? highlightColor; + + const CircleSkeleton({ + super.key, + required this.size, + this.margin, + this.backgroundColor, + this.highlightColor, + }); + + @override + State createState() => _CircleSkeletonState(); +} + +class _CircleSkeletonState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(); + + _animation = Tween(begin: -2, end: 2).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOutSine, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final baseColor = widget.backgroundColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.5); + final highlightColor = widget.highlightColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.8); + + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: widget.size, + height: widget.size, + margin: widget.margin, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment(_animation.value, 0), + end: Alignment(_animation.value + 1, 0), + colors: [ + baseColor, + highlightColor, + baseColor, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ); + }, + ); + } +} + +/// 图片骨架屏组件 - 专门用于图片加载时的占位显示 +/// 模拟真实的图片比例和加载效果 +/// [width] 图片宽度,null时根据父容器自适应 +/// [height] 图片高度,必填参数 +/// [aspectRatio] 宽高比,null时使用固定高度 +class ImageSkeleton extends StatelessWidget { + /// 图片宽度,null时根据父容器自适应 + final double? width; + + /// 图片高度,必填参数 + final double height; + + /// 宽高比,null时使用固定高度 + final double? aspectRatio; + + /// 外边距,控制骨架屏与其他元素的间距 + final EdgeInsetsGeometry? margin; + + /// 背景颜色,null时使用主题色彩 + final Color? backgroundColor; + + /// 骨架屏高亮颜色,null时使用主题色彩 + final Color? highlightColor; + + const ImageSkeleton({ + super.key, + this.width, + required this.height, + this.aspectRatio, + this.margin, + this.backgroundColor, + this.highlightColor, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final baseColor = backgroundColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.5); + final highlightColor = this.highlightColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.8); + + Widget skeleton = Container( + width: width, + height: height, + margin: margin, + decoration: BoxDecoration( + color: baseColor, + borderRadius: BorderRadius.circular(8), + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 1500), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + baseColor, + highlightColor, + baseColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + ), + ); + + if (aspectRatio != null) { + skeleton = AspectRatio( + aspectRatio: aspectRatio!, + child: skeleton, + ); + } + + return skeleton; + } +} + +/// 文本骨架屏组件 - 用于文本内容加载时的占位显示 +/// 模拟不同长度的文本行 +/// [width] 文本宽度,null时根据父容器自适应 +/// [height] 文本高度,null时使用默认行高 +/// [lineCount] 文本行数,大于1时显示多行 +class TextSkeleton extends StatelessWidget { + /// 文本宽度,null时根据父容器自适应 + final double? width; + + /// 文本高度,null时使用默认行高 + final double? height; + + /// 文本行数,大于1时显示多行 + final int lineCount; + + /// 行间距,控制多行文本的间距 + final double lineSpacing; + + /// 外边距,控制骨架屏与其他元素的间距 + final EdgeInsetsGeometry? margin; + + /// 背景颜色,null时使用主题色彩 + final Color? backgroundColor; + + /// 骨架屏高亮颜色,null时使用主题色彩 + final Color? highlightColor; + + const TextSkeleton({ + super.key, + this.width, + this.height, + this.lineCount = 1, + this.lineSpacing = 8, + this.margin, + this.backgroundColor, + this.highlightColor, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final baseColor = backgroundColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.5); + final highlightColor = this.highlightColor ?? + theme.colorScheme.surfaceVariant.withOpacity(0.8); + + final textHeight = height ?? theme.textTheme.bodyMedium?.fontSize ?? 14; + + if (lineCount == 1) { + return Container( + width: width, + height: textHeight, + margin: margin, + decoration: BoxDecoration( + color: baseColor, + borderRadius: BorderRadius.circular(4), + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 1500), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + baseColor, + highlightColor, + baseColor, + ], + ), + borderRadius: BorderRadius.circular(4), + ), + ), + ); + } + + return Container( + margin: margin, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(lineCount, (index) { + // 最后一行宽度减短,模拟真实的文本效果 + final lineWidth = index == lineCount - 1 ? (width ?? double.infinity) * 0.7 : width; + + return Padding( + padding: EdgeInsets.only(bottom: index < lineCount - 1 ? lineSpacing : 0), + child: Container( + width: lineWidth, + height: textHeight, + decoration: BoxDecoration( + color: baseColor, + borderRadius: BorderRadius.circular(4), + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 1500), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + baseColor, + highlightColor, + baseColor, + ], + stops: const [0.0, 0.5, 1.0], + ), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ); + }), + ), + ); + } +} + +/// 圆形进度指示器 - 标准的圆形加载动画 +/// [size] 指示器大小,null时使用默认大小 +/// [strokeWidth] 线条宽度,null时使用默认宽度 +/// [color] 指示器颜色,null时使用主题主色调 +class LoadingIndicator extends StatelessWidget { + /// 指示器大小,null时使用默认大小 + final double? size; + + /// 线条宽度,null时使用默认宽度 + final double? strokeWidth; + + /// 指示器颜色,null时使用主题主色调 + final Color? color; + + const LoadingIndicator({ + super.key, + this.size, + this.strokeWidth, + this.color, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final indicatorColor = color ?? theme.colorScheme.primary; + final indicatorSize = size ?? 24; + + return SizedBox( + width: indicatorSize, + height: indicatorSize, + child: CircularProgressIndicator( + strokeWidth: strokeWidth ?? 2, + valueColor: AlwaysStoppedAnimation(indicatorColor), + ), + ); + } +} + +/// 线性进度指示器 - 水平进度条加载动画 +/// [width] 进度条宽度,null时根据父容器自适应 +/// [height] 进度条高度,null时使用默认高度 +/// [color] 进度条颜色,null时使用主题主色调 +class LinearLoadingIndicator extends StatelessWidget { + /// 进度条宽度,null时根据父容器自适应 + final double? width; + + /// 进度条高度,null时使用默认高度 + final double? height; + + /// 进度条颜色,null时使用主题主色调 + final Color? color; + + /// 背景颜色,null时使用主题背景色 + final Color? backgroundColor; + + /// 是否显示百分比文本 + final bool showPercentage; + + const LinearLoadingIndicator({ + super.key, + this.width, + this.height, + this.color, + this.backgroundColor, + this.showPercentage = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final progressColor = color ?? theme.colorScheme.primary; + final bgColor = backgroundColor ?? theme.colorScheme.surfaceVariant; + final progressHeight = height ?? 4; + + Widget progressBar = Container( + width: width, + height: progressHeight, + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(progressHeight / 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(progressHeight / 2), + child: LinearProgressIndicator( + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation(progressColor), + minHeight: progressHeight, + ), + ), + ); + + if (showPercentage) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + progressBar, + const SizedBox(height: 8), + Text( + '加载中...', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ); + } + + return progressBar; + } +} + +/// 加载状态包装器 - 根据加载状态显示不同内容 +/// [isLoading] 是否处于加载状态 +/// [loadingWidget] 自定义加载组件,null时使用默认骨架屏 +/// [child] 正常内容组件 +/// [skeleton] 自定义骨架屏,null时使用默认骨架屏 +class LoadingWrapper extends StatelessWidget { + /// 是否处于加载状态 + final bool isLoading; + + /// 自定义加载组件,null时使用默认骨架屏 + final Widget? loadingWidget; + + /// 正常内容组件 + final Widget child; + + /// 自定义骨架屏,null时使用默认骨架屏 + final Widget? skeleton; + + const LoadingWrapper({ + super.key, + required this.isLoading, + required this.child, + this.loadingWidget, + this.skeleton, + }); + + @override + Widget build(BuildContext context) { + if (isLoading) { + return loadingWidget ?? skeleton ?? Container(); + } + return child; + } +} + +/// 骨架屏列表 - 用于列表加载时的占位显示 +/// [itemCount] 列表项数量 +/// [itemHeight] 列表项高度 +/// [itemSpacing] 列表项间距 +/// [padding] 列表内边距 +class SkeletonList extends StatelessWidget { + /// 列表项数量 + final int itemCount; + + /// 列表项高度 + final double itemHeight; + + /// 列表项间距 + final double itemSpacing; + + /// 列表内边距 + final EdgeInsetsGeometry? padding; + + /// 是否显示圆形头像占位 + final bool showAvatar; + + /// 是否显示多行文本占位 + final bool showMultiLineText; + + const SkeletonList({ + super.key, + required this.itemCount, + this.itemHeight = 80, + this.itemSpacing = 8, + this.padding, + this.showAvatar = true, + this.showMultiLineText = true, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: padding, + itemCount: itemCount, + itemBuilder: (context, index) { + return Padding( + padding: EdgeInsets.only(bottom: itemSpacing), + child: Container( + height: itemHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showAvatar) ...[ + CircleSkeleton(size: itemHeight * 0.6), + const SizedBox(width: 12), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextSkeleton( + width: double.infinity, + height: 16, + lineCount: showMultiLineText ? 2 : 1, + lineSpacing: 6, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +/// 骨架屏网格 - 用于网格布局加载时的占位显示 +/// [crossAxisCount] 交叉轴数量 +/// [itemAspectRatio] 列表项宽高比 +/// [itemSpacing] 列表项间距 +/// [padding] 网格内边距 +class SkeletonGrid extends StatelessWidget { + /// 交叉轴数量 + final int crossAxisCount; + + /// 列表项宽高比 + final double itemAspectRatio; + + /// 列表项间距 + final double itemSpacing; + + /// 网格内边距 + final EdgeInsetsGeometry? padding; + + /// 列表项数量 + final int itemCount; + + const SkeletonGrid({ + super.key, + required this.crossAxisCount, + this.itemAspectRatio = 1.0, + this.itemSpacing = 8, + this.padding, + this.itemCount = 6, + }); + + @override + Widget build(BuildContext context) { + return GridView.builder( + padding: padding, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + childAspectRatio: itemAspectRatio, + crossAxisSpacing: itemSpacing, + mainAxisSpacing: itemSpacing, + ), + itemCount: itemCount, + itemBuilder: (context, index) { + return const Skeleton( + width: double.infinity, + height: double.infinity, + ); + }, + ); + } +} + +/// 全屏加载组件 - 覆盖整个屏幕的加载状态 +/// [message] 加载提示文本 +/// [showProgress] 是否显示进度指示器 +/// [backgroundColor] 背景颜色,null时使用半透明黑色 +class FullScreenLoading extends StatelessWidget { + /// 加载提示文本 + final String? message; + + /// 是否显示进度指示器 + final bool showProgress; + + /// 背景颜色,null时使用半透明黑色 + final Color? backgroundColor; + + /// 文本颜色,null时使用白色 + final Color? textColor; + + const FullScreenLoading({ + super.key, + this.message, + this.showProgress = true, + this.backgroundColor, + this.textColor, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final bgColor = backgroundColor ?? Colors.black.withOpacity(0.7); + final textColor = this.textColor ?? Colors.white; + + return Container( + color: bgColor, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (showProgress) ...[ + LoadingIndicator( + size: 48, + color: textColor, + ), + const SizedBox(height: 16), + ], + if (message != null) ...[ + Text( + message!, + style: theme.textTheme.bodyLarge?.copyWith( + color: textColor, + ), + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ); + } +} + +/// 加载状态枚举 - 定义不同的加载状态 +enum LoadingState { + /// 初始加载状态 + initial, + + /// 加载中状态 + loading, + + /// 加载完成状态 + completed, + + /// 加载失败状态 + error, + + /// 空数据状态 + empty, +} + +/// 加载状态构建器 - 根据加载状态构建不同的UI +/// [state] 当前加载状态 +/// [loadingBuilder] 加载状态构建器 +/// [completedBuilder] 完成状态构建器 +/// [errorBuilder] 错误状态构建器 +/// [emptyBuilder] 空状态构建器 +/// [initialBuilder] 初始状态构建器 +class LoadingStateBuilder extends StatelessWidget { + /// 当前加载状态 + final LoadingState state; + + /// 加载状态构建器 + final WidgetBuilder? loadingBuilder; + + /// 完成状态构建器 + final WidgetBuilder completedBuilder; + + /// 错误状态构建器 + final WidgetBuilder? errorBuilder; + + /// 空状态构建器 + final WidgetBuilder? emptyBuilder; + + /// 初始状态构建器 + final WidgetBuilder? initialBuilder; + + const LoadingStateBuilder({ + super.key, + required this.state, + required this.completedBuilder, + this.loadingBuilder, + this.errorBuilder, + this.emptyBuilder, + this.initialBuilder, + }); + + @override + Widget build(BuildContext context) { + switch (state) { + case LoadingState.initial: + return initialBuilder?.call(context) ?? Container(); + case LoadingState.loading: + return loadingBuilder?.call(context) ?? + const Center(child: LoadingIndicator()); + case LoadingState.completed: + return completedBuilder(context); + case LoadingState.error: + return errorBuilder?.call(context) ?? + const Center(child: Text('加载失败,请重试')); + case LoadingState.empty: + return emptyBuilder?.call(context) ?? + const Center(child: Text('暂无数据')); + } + } +} \ No newline at end of file