329 lines
8.1 KiB
Dart
329 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../constants/app_constants.dart';
|
|
|
|
// 自定义按钮组件
|
|
class CustomButton extends StatelessWidget {
|
|
final String text;
|
|
final VoidCallback? onPressed;
|
|
final IconData? icon;
|
|
final Color? backgroundColor;
|
|
final Color? textColor;
|
|
final double? borderRadius;
|
|
final bool isLoading;
|
|
final double? width;
|
|
final double? height;
|
|
|
|
const CustomButton({
|
|
super.key,
|
|
required this.text,
|
|
this.onPressed,
|
|
this.icon,
|
|
this.backgroundColor,
|
|
this.textColor,
|
|
this.borderRadius,
|
|
this.isLoading = false,
|
|
this.width,
|
|
this.height,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: width,
|
|
height: height ?? 48,
|
|
child: ElevatedButton(
|
|
onPressed: isLoading ? null : onPressed,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: backgroundColor ?? AppConstants.primaryColor,
|
|
foregroundColor: textColor ?? Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(borderRadius ?? 8),
|
|
),
|
|
elevation: 2,
|
|
),
|
|
child: isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
)
|
|
: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, size: 18),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Text(text),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 自定义卡片组件
|
|
class CustomCard extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry? padding;
|
|
final EdgeInsetsGeometry? margin;
|
|
final double? borderRadius;
|
|
final Color? backgroundColor;
|
|
final VoidCallback? onTap;
|
|
final BoxBorder? border;
|
|
|
|
const CustomCard({
|
|
super.key,
|
|
required this.child,
|
|
this.padding,
|
|
this.margin,
|
|
this.borderRadius,
|
|
this.backgroundColor,
|
|
this.onTap,
|
|
this.border,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
margin: margin ?? const EdgeInsets.all(8),
|
|
child: Material(
|
|
color: backgroundColor ?? AppConstants.surfaceColor,
|
|
borderRadius: BorderRadius.circular(borderRadius ?? 12),
|
|
elevation: 2,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(borderRadius ?? 12),
|
|
child: Container(
|
|
padding: padding ?? const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(borderRadius ?? 12),
|
|
border: border,
|
|
),
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 自定义输入框组件
|
|
class CustomTextField extends StatelessWidget {
|
|
final String? labelText;
|
|
final String? hintText;
|
|
final TextEditingController? controller;
|
|
final IconData? prefixIcon;
|
|
final IconData? suffixIcon;
|
|
final VoidCallback? onSuffixIconTap;
|
|
final bool obscureText;
|
|
final String? Function(String?)? validator;
|
|
final void Function(String)? onChanged;
|
|
final TextInputType? keyboardType;
|
|
final int? maxLines;
|
|
final bool enabled;
|
|
|
|
const CustomTextField({
|
|
super.key,
|
|
this.labelText,
|
|
this.hintText,
|
|
this.controller,
|
|
this.prefixIcon,
|
|
this.suffixIcon,
|
|
this.onSuffixIconTap,
|
|
this.obscureText = false,
|
|
this.validator,
|
|
this.onChanged,
|
|
this.keyboardType,
|
|
this.maxLines = 1,
|
|
this.enabled = true,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextFormField(
|
|
controller: controller,
|
|
obscureText: obscureText,
|
|
validator: validator,
|
|
onChanged: onChanged,
|
|
keyboardType: keyboardType,
|
|
maxLines: maxLines,
|
|
enabled: enabled,
|
|
decoration: InputDecoration(
|
|
labelText: labelText,
|
|
hintText: hintText,
|
|
prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
|
|
suffixIcon: suffixIcon != null
|
|
? IconButton(
|
|
icon: Icon(suffixIcon),
|
|
onPressed: onSuffixIconTap,
|
|
)
|
|
: null,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 加载指示器组件
|
|
class LoadingIndicator extends StatelessWidget {
|
|
final String? message;
|
|
final Color? color;
|
|
|
|
const LoadingIndicator({
|
|
super.key,
|
|
this.message,
|
|
this.color,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
color ?? AppConstants.primaryColor,
|
|
),
|
|
),
|
|
if (message != null) ...[
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
message!,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 空状态组件
|
|
class EmptyState extends StatelessWidget {
|
|
final String title;
|
|
final String? subtitle;
|
|
final IconData? icon;
|
|
final Widget? action;
|
|
|
|
const EmptyState({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.icon,
|
|
this.action,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
icon ?? Icons.inbox_outlined,
|
|
size: 64,
|
|
color: Colors.grey[400],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (subtitle != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
subtitle!,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
if (action != null) ...[
|
|
const SizedBox(height: 24),
|
|
action!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 错误状态组件
|
|
class ErrorState extends StatelessWidget {
|
|
final String title;
|
|
final String? subtitle;
|
|
final VoidCallback? onRetry;
|
|
|
|
const ErrorState({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.onRetry,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.error_outline,
|
|
size: 64,
|
|
color: AppConstants.errorColor,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (subtitle != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
subtitle!,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
if (onRetry != null) ...[
|
|
const SizedBox(height: 24),
|
|
CustomButton(
|
|
text: AppConstants.retryText,
|
|
onPressed: onRetry,
|
|
icon: AppConstants.refreshIcon,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|