基础ui组件

This commit is contained in:
ddshi 2025-09-17 11:42:38 +08:00
parent 8005e21110
commit 1212c4ea27
8 changed files with 2709 additions and 50 deletions

View File

@ -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警告)
- 分享接收机制配置完成
- Android分享接收配置完成
- iOS分享接收配置完成
- 多张图片接收逻辑处理
- 分享功能测试通过

View File

@ -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),
);
}
}

View File

@ -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';
}

View File

@ -119,16 +119,6 @@ class HiveDatabase {
}
}
/// 创建默认数据 - 初始化数据库时的默认数据创建
/// 创建系统必需的默认文件夹和标签
static Future<void> _createDefaultData() async {
final foldersBox = Hive.box<HiveImageFolder>(_foldersBoxName);
final tagsBox = Hive.box<HiveImageTag>(_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<bool> 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<bool> 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<bool> 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<Map<String, int>> 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,
};

View File

@ -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<Color>(
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<Color>((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<Color>((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<Color>((states) {
if (states.contains(MaterialState.disabled)) {
return (backgroundColor ?? theme.colorScheme.secondaryContainer)
.withOpacity(0.12);
}
return backgroundColor ?? theme.colorScheme.secondaryContainer;
}),
foregroundColor: MaterialStateProperty.resolveWith<Color>((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<Color>((states) {
if (states.contains(MaterialState.disabled)) {
return (backgroundColor ?? AppColors.error).withOpacity(0.12);
}
return backgroundColor ?? AppColors.error;
}),
foregroundColor: MaterialStateProperty.resolveWith<Color>((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<Color>((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,
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<SimpleShimmer> createState() => _SimpleShimmerState();
}
class _SimpleShimmerState extends State<SimpleShimmer> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
)..repeat();
_animation = Tween<double>(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<Skeleton> createState() => _SkeletonState();
}
class _SkeletonState extends State<Skeleton> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(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<CircleSkeleton> createState() => _CircleSkeletonState();
}
class _CircleSkeletonState extends State<CircleSkeleton> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(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<Color>(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<Color>(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('暂无数据'));
}
}
}