Files
ai_english/client/lib/shared/widgets/loading_widget.dart

390 lines
9.9 KiB
Dart
Raw Normal View History

2025-11-17 14:09:17 +08:00
import 'package:flutter/material.dart';
/// 加载组件
class LoadingWidget extends StatelessWidget {
final String? message;
final double? size;
final Color? color;
final EdgeInsetsGeometry? padding;
final bool showMessage;
const LoadingWidget({
super.key,
this.message,
this.size,
this.color,
this.padding,
this.showMessage = true,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: padding ?? const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size ?? 32,
height: size ?? 32,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
color ?? theme.colorScheme.primary,
),
),
),
if (showMessage && message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
],
),
);
}
}
/// 页面加载组件
class PageLoadingWidget extends StatelessWidget {
final String? message;
final bool showAppBar;
final String? title;
const PageLoadingWidget({
super.key,
this.message,
this.showAppBar = false,
this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: showAppBar
? AppBar(
title: title != null ? Text(title!) : null,
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
)
: null,
body: Center(
child: LoadingWidget(
message: message ?? '加载中...',
size: 48,
),
),
);
}
}
/// 列表加载组件
class ListLoadingWidget extends StatelessWidget {
final int itemCount;
final double itemHeight;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
const ListLoadingWidget({
super.key,
this.itemCount = 5,
this.itemHeight = 80,
this.padding,
this.margin,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: padding,
itemCount: itemCount,
itemBuilder: (context, index) => Container(
height: itemHeight,
margin: margin ?? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: const ShimmerWidget(),
),
);
}
}
/// 骨架屏组件
class ShimmerWidget extends StatefulWidget {
final double? width;
final double? height;
final BorderRadius? borderRadius;
final Color? baseColor;
final Color? highlightColor;
const ShimmerWidget({
super.key,
this.width,
this.height,
this.borderRadius,
this.baseColor,
this.highlightColor,
});
@override
State<ShimmerWidget> createState() => _ShimmerWidgetState();
}
class _ShimmerWidgetState extends State<ShimmerWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = Tween<double>(
begin: -1.0,
end: 2.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final baseColor = widget.baseColor ?? theme.colorScheme.surfaceVariant;
final highlightColor = widget.highlightColor ??
theme.colorScheme.surface.withOpacity(0.8);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
baseColor,
highlightColor,
baseColor,
],
stops: [
_animation.value - 0.3,
_animation.value,
_animation.value + 0.3,
],
),
),
);
},
);
}
}
/// 卡片骨架屏
class CardShimmerWidget extends StatelessWidget {
final EdgeInsetsGeometry? margin;
final double? height;
const CardShimmerWidget({
super.key,
this.margin,
this.height,
});
@override
Widget build(BuildContext context) {
return Container(
margin: margin ?? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const ShimmerWidget(
width: 40,
height: 40,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerWidget(
width: double.infinity,
height: 16,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
ShimmerWidget(
width: 120,
height: 12,
borderRadius: BorderRadius.circular(4),
),
],
),
),
],
),
if (height != null && height! > 100) ...[
const SizedBox(height: 16),
ShimmerWidget(
width: double.infinity,
height: 12,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
ShimmerWidget(
width: double.infinity,
height: 12,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
ShimmerWidget(
width: 200,
height: 12,
borderRadius: BorderRadius.circular(4),
),
],
],
),
);
}
}
/// 按钮加载状态
class LoadingButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final Color? backgroundColor;
final Color? foregroundColor;
final EdgeInsetsGeometry? padding;
final double? width;
final double? height;
final BorderRadius? borderRadius;
const LoadingButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.icon,
this.backgroundColor,
this.foregroundColor,
this.padding,
this.width,
this.height,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: width,
height: height ?? 48,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? theme.colorScheme.primary,
foregroundColor: foregroundColor ?? theme.colorScheme.onPrimary,
padding: padding ?? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.circular(8),
),
elevation: 0,
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
foregroundColor ?? theme.colorScheme.onPrimary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
const SizedBox(width: 8),
],
Text(
text,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
/// 刷新指示器
class RefreshIndicatorWidget extends StatelessWidget {
final Widget child;
final Future<void> Function() onRefresh;
final String? refreshText;
const RefreshIndicatorWidget({
super.key,
required this.child,
required this.onRefresh,
this.refreshText,
});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
color: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surface,
child: child,
);
}
}