init
This commit is contained in:
194
client/lib/core/widgets/custom_button.dart
Normal file
194
client/lib/core/widgets/custom_button.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
265
client/lib/core/widgets/custom_text_field.dart
Normal file
265
client/lib/core/widgets/custom_text_field.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
106
client/lib/core/widgets/not_found_screen.dart
Normal file
106
client/lib/core/widgets/not_found_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user