This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_dimensions.dart';
/// 自定义按钮组件
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isOutlined;
final Color? backgroundColor;
final Color? textColor;
final IconData? icon;
final double? width;
final double? height;
final EdgeInsetsGeometry? padding;
const CustomButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isOutlined = false,
this.backgroundColor,
this.textColor,
this.icon,
this.width,
this.height,
this.padding,
});
@override
Widget build(BuildContext context) {
final effectiveBackgroundColor = backgroundColor ??
(isOutlined ? Colors.transparent : AppColors.primary);
final effectiveTextColor = textColor ??
(isOutlined ? AppColors.primary : AppColors.onPrimary);
final effectivePadding = padding ?? EdgeInsets.symmetric(
vertical: AppDimensions.spacingMd,
horizontal: AppDimensions.spacingLg,
);
Widget buttonChild = isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveTextColor),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(
icon,
color: effectiveTextColor,
size: 20,
),
SizedBox(width: AppDimensions.spacingSm),
],
Text(
text,
style: AppTextStyles.bodyLarge.copyWith(
color: effectiveTextColor,
fontWeight: FontWeight.w600,
),
),
],
);
if (isOutlined) {
return SizedBox(
width: width,
height: height,
child: OutlinedButton(
onPressed: isLoading ? null : onPressed,
style: OutlinedButton.styleFrom(
side: BorderSide(
color: onPressed != null ? AppColors.primary : AppColors.onSurface.withOpacity(0.3),
width: 1.5,
),
padding: effectivePadding,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: buttonChild,
),
);
}
return SizedBox(
width: width,
height: height,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: effectiveBackgroundColor,
foregroundColor: effectiveTextColor,
padding: effectivePadding,
elevation: 2,
shadowColor: AppColors.primary.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
disabledBackgroundColor: AppColors.onSurface.withOpacity(0.12),
disabledForegroundColor: AppColors.onSurface.withOpacity(0.38),
),
child: buttonChild,
),
);
}
}
/// 图标按钮组件
class CustomIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback? onPressed;
final Color? backgroundColor;
final Color? iconColor;
final double? size;
final String? tooltip;
final bool isLoading;
const CustomIconButton({
super.key,
required this.icon,
this.onPressed,
this.backgroundColor,
this.iconColor,
this.size,
this.tooltip,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
final effectiveSize = size ?? 48.0;
final effectiveBackgroundColor = backgroundColor ?? AppColors.primary;
final effectiveIconColor = iconColor ?? AppColors.onPrimary;
Widget iconWidget = isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveIconColor),
),
)
: Icon(
icon,
color: effectiveIconColor,
size: effectiveSize * 0.5,
);
Widget button = Container(
width: effectiveSize,
height: effectiveSize,
decoration: BoxDecoration(
color: effectiveBackgroundColor,
borderRadius: BorderRadius.circular(effectiveSize / 2),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(effectiveSize / 2),
child: Center(child: iconWidget),
),
),
);
if (tooltip != null) {
return Tooltip(
message: tooltip!,
child: button,
);
}
return button;
}
}

View File

@@ -0,0 +1,265 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_dimensions.dart';
/// 自定义文本输入框组件
class CustomTextField extends StatefulWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final String? helperText;
final String? errorText;
final IconData? prefixIcon;
final Widget? suffixIcon;
final bool obscureText;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final int? maxLines;
final int? minLines;
final int? maxLength;
final bool enabled;
final bool readOnly;
final bool autofocus;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function()? onTap;
final void Function(String)? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
final EdgeInsetsGeometry? contentPadding;
final TextStyle? textStyle;
final TextStyle? labelStyle;
final TextStyle? hintStyle;
final Color? fillColor;
final Color? borderColor;
final Color? focusedBorderColor;
final Color? errorBorderColor;
final double? borderRadius;
final bool filled;
const CustomTextField({
super.key,
this.controller,
this.labelText,
this.hintText,
this.helperText,
this.errorText,
this.prefixIcon,
this.suffixIcon,
this.obscureText = false,
this.keyboardType,
this.textInputAction,
this.maxLines = 1,
this.minLines,
this.maxLength,
this.enabled = true,
this.readOnly = false,
this.autofocus = false,
this.validator,
this.onChanged,
this.onTap,
this.onSubmitted,
this.inputFormatters,
this.focusNode,
this.contentPadding,
this.textStyle,
this.labelStyle,
this.hintStyle,
this.fillColor,
this.borderColor,
this.focusedBorderColor,
this.errorBorderColor,
this.borderRadius,
this.filled = true,
});
@override
State<CustomTextField> createState() => _CustomTextFieldState();
}
class _CustomTextFieldState extends State<CustomTextField> {
late FocusNode _focusNode;
bool _isFocused = false;
@override
void initState() {
super.initState();
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onFocusChange);
}
@override
void dispose() {
if (widget.focusNode == null) {
_focusNode.dispose();
} else {
_focusNode.removeListener(_onFocusChange);
}
super.dispose();
}
void _onFocusChange() {
setState(() {
_isFocused = _focusNode.hasFocus;
});
}
@override
Widget build(BuildContext context) {
final effectiveBorderRadius = widget.borderRadius ?? 8.0;
final effectiveFillColor = widget.fillColor ?? AppColors.surface;
final effectiveBorderColor = widget.borderColor ?? AppColors.onSurface.withOpacity(0.3);
final effectiveFocusedBorderColor = widget.focusedBorderColor ?? AppColors.primary;
final effectiveErrorBorderColor = widget.errorBorderColor ?? AppColors.error;
final effectiveContentPadding = widget.contentPadding ?? EdgeInsets.symmetric(
horizontal: AppDimensions.spacingMd,
vertical: AppDimensions.spacingMd,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.labelText != null) ...[
Text(
widget.labelText!,
style: widget.labelStyle ?? AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: AppDimensions.spacingSm),
],
TextFormField(
controller: widget.controller,
focusNode: _focusNode,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
maxLines: widget.maxLines,
minLines: widget.minLines,
maxLength: widget.maxLength,
enabled: widget.enabled,
readOnly: widget.readOnly,
autofocus: widget.autofocus,
validator: widget.validator,
onChanged: widget.onChanged,
onTap: widget.onTap,
onFieldSubmitted: widget.onSubmitted,
inputFormatters: widget.inputFormatters,
style: widget.textStyle ?? AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface,
),
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: widget.hintStyle ?? AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface.withOpacity(0.6),
),
helperText: widget.helperText,
errorText: widget.errorText,
prefixIcon: widget.prefixIcon != null
? Icon(
widget.prefixIcon,
color: _isFocused ? effectiveFocusedBorderColor : AppColors.onSurface.withOpacity(0.6),
)
: null,
suffixIcon: widget.suffixIcon,
filled: widget.filled,
fillColor: effectiveFillColor,
contentPadding: effectiveContentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveBorderColor,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveBorderColor,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveFocusedBorderColor,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveErrorBorderColor,
width: 1.0,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveErrorBorderColor,
width: 2.0,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: AppColors.onSurface.withOpacity(0.12),
width: 1.0,
),
),
),
),
],
);
}
}
/// 搜索输入框组件
class SearchTextField extends StatelessWidget {
final TextEditingController? controller;
final String? hintText;
final void Function(String)? onChanged;
final void Function(String)? onSubmitted;
final VoidCallback? onClear;
final bool autofocus;
final FocusNode? focusNode;
const SearchTextField({
super.key,
this.controller,
this.hintText,
this.onChanged,
this.onSubmitted,
this.onClear,
this.autofocus = false,
this.focusNode,
});
@override
Widget build(BuildContext context) {
return CustomTextField(
controller: controller,
hintText: hintText ?? '搜索...',
prefixIcon: Icons.search,
suffixIcon: controller?.text.isNotEmpty == true
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller?.clear();
onClear?.call();
},
)
: null,
onChanged: onChanged,
onSubmitted: onSubmitted,
autofocus: autofocus,
focusNode: focusNode,
textInputAction: TextInputAction.search,
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_dimensions.dart';
/// 404页面 - 页面未找到
class NotFoundScreen extends StatelessWidget {
final String? routeName;
const NotFoundScreen({
super.key,
this.routeName,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('页面未找到'),
backgroundColor: AppColors.surface,
foregroundColor: AppColors.onSurface,
),
body: Center(
child: Padding(
padding: EdgeInsets.all(AppDimensions.pagePadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 404图标
Icon(
Icons.error_outline,
size: AppDimensions.iconXxl * 2,
color: AppColors.outline,
),
SizedBox(height: AppDimensions.spacingXl),
// 标题
Text(
'页面未找到',
style: AppTextStyles.headlineLarge.copyWith(
color: AppColors.onSurface,
),
textAlign: TextAlign.center,
),
SizedBox(height: AppDimensions.spacingMd),
// 描述
Text(
routeName != null
? '路由 "$routeName" 不存在'
: '您访问的页面不存在或已被移除',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
SizedBox(height: AppDimensions.spacingXxl),
// 返回按钮
ElevatedButton.icon(
onPressed: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
Navigator.of(context).pushReplacementNamed('/');
}
},
icon: const Icon(Icons.arrow_back),
label: const Text('返回'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
padding: EdgeInsets.symmetric(
horizontal: AppDimensions.spacingXl,
vertical: AppDimensions.spacingMd,
),
),
),
SizedBox(height: AppDimensions.spacingMd),
// 回到首页按钮
TextButton(
onPressed: () {
Navigator.of(context).pushNamedAndRemoveUntil(
'/',
(route) => false,
);
},
child: Text(
'回到首页',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.primary,
),
),
),
],
),
),
),
);
}
}