- 创建 MaterialInput 和 MaterialPasswordInput 组件 - 移除 shadcn_ui 依赖,使用原生 Material 组件 - 添加圆角边框 + Focus 效果 - 添加浮动标签动画 - 改进登录和注册页面视觉体验 - 统一设计语言(Material Design 3)
231 lines
6.7 KiB
Dart
231 lines
6.7 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import '../theme/app_color_scheme.dart';
|
||
import '../theme/app_spacing.dart';
|
||
import '../theme/app_theme.dart';
|
||
import '../theme/app_theme_extension.dart';
|
||
|
||
/// Material Design 3 风格的输入框组件
|
||
///
|
||
/// 特点:
|
||
/// - 圆角外边框(OutlineInputBorder)
|
||
/// - Focus 时边框颜色变化
|
||
/// - 标签上浮动画
|
||
/// - 统一的视觉效果
|
||
class MaterialInput extends StatelessWidget {
|
||
final TextEditingController? controller;
|
||
final String? labelText;
|
||
final String? hintText;
|
||
final IconData? prefixIcon;
|
||
final Widget? suffixIcon;
|
||
final bool obscureText;
|
||
final TextInputType? keyboardType;
|
||
final int? maxLines;
|
||
final int? maxLength;
|
||
final bool enabled;
|
||
final bool readOnly;
|
||
final String? Function(String?)? validator;
|
||
final ValueChanged<String>? onChanged;
|
||
final VoidCallback? onTap;
|
||
final ValueChanged<String>? onSubmitted;
|
||
final List<TextInputFormatter>? inputFormatters;
|
||
final FocusNode? focusNode;
|
||
|
||
const MaterialInput({
|
||
super.key,
|
||
this.controller,
|
||
this.labelText,
|
||
this.hintText,
|
||
this.prefixIcon,
|
||
this.suffixIcon,
|
||
this.obscureText = false,
|
||
this.keyboardType,
|
||
this.maxLines = 1,
|
||
this.maxLength,
|
||
this.enabled = true,
|
||
this.readOnly = false,
|
||
this.validator,
|
||
this.onChanged,
|
||
this.onTap,
|
||
this.onSubmitted,
|
||
this.inputFormatters,
|
||
this.focusNode,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||
|
||
// Material Design 3 的颜色
|
||
final primaryColor = isDark ? AppColorScheme.up : colorScheme.primary;
|
||
final borderColor = isDark
|
||
? AppColorScheme.darkOutline.withValues(alpha: 0.3)
|
||
: colorScheme.outline.withValues(alpha: 0.5);
|
||
final fillColor = isDark
|
||
? AppColorScheme.darkSurfaceContainerHigh.withValues(alpha: 0.3)
|
||
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5);
|
||
|
||
return TextFormField(
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
obscureText: obscureText,
|
||
keyboardType: keyboardType,
|
||
maxLines: maxLines,
|
||
maxLength: maxLength,
|
||
enabled: enabled,
|
||
readOnly: readOnly,
|
||
validator: validator,
|
||
onChanged: onChanged,
|
||
onTap: onTap,
|
||
onFieldSubmitted: onSubmitted,
|
||
inputFormatters: inputFormatters,
|
||
style: AppTextStyles.headlineMedium(context).copyWith(
|
||
color: enabled
|
||
? colorScheme.onSurface
|
||
: colorScheme.onSurface.withValues(alpha: 0.5),
|
||
),
|
||
cursorColor: primaryColor,
|
||
cursorWidth: 2.0,
|
||
cursorHeight: 20,
|
||
decoration: InputDecoration(
|
||
// 标签(Material Design 3 的浮动标签)
|
||
labelText: labelText,
|
||
labelStyle: AppTextStyles.bodyLarge(context).copyWith(
|
||
color: colorScheme.onSurfaceVariant,
|
||
),
|
||
floatingLabelStyle: AppTextStyles.bodyMedium(context).copyWith(
|
||
color: primaryColor,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
|
||
// 提示文本
|
||
hintText: hintText,
|
||
hintStyle: AppTextStyles.bodyLarge(context).copyWith(
|
||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||
),
|
||
|
||
// 前置图标
|
||
prefixIcon: prefixIcon != null
|
||
? Icon(
|
||
prefixIcon,
|
||
color: colorScheme.onSurfaceVariant,
|
||
size: 22,
|
||
)
|
||
: null,
|
||
prefixIconColor: WidgetStateColor.resolveWith((states) {
|
||
if (states.contains(WidgetState.focused)) {
|
||
return primaryColor;
|
||
}
|
||
return colorScheme.onSurfaceVariant;
|
||
}),
|
||
|
||
// 后置图标
|
||
suffixIcon: suffixIcon,
|
||
|
||
// 填充颜色
|
||
filled: true,
|
||
fillColor: fillColor,
|
||
|
||
// 内容内边距
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.lg,
|
||
vertical: AppSpacing.md,
|
||
),
|
||
|
||
// Material Design 3 边框样式
|
||
border: _buildBorder(borderColor, AppRadius.lg),
|
||
enabledBorder: _buildBorder(borderColor, AppRadius.lg),
|
||
focusedBorder: _buildBorder(primaryColor, AppRadius.lg, width: 2.0),
|
||
errorBorder: _buildBorder(colorScheme.error, AppRadius.lg),
|
||
focusedErrorBorder: _buildBorder(colorScheme.error, AppRadius.lg, width: 2.0),
|
||
disabledBorder: _buildBorder(
|
||
borderColor.withValues(alpha: 0.3),
|
||
AppRadius.lg,
|
||
),
|
||
|
||
// 错误文本样式
|
||
errorStyle: AppTextStyles.bodySmall(context).copyWith(
|
||
color: colorScheme.error,
|
||
),
|
||
|
||
// 计数器样式
|
||
counterStyle: AppTextStyles.bodySmall(context).copyWith(
|
||
color: colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
OutlineInputBorder _buildBorder(Color color, double radius, {double width = 1.0}) {
|
||
return OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(radius),
|
||
borderSide: BorderSide(
|
||
color: color,
|
||
width: width,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Material Design 3 风格的密码输入框(带显示/隐藏切换)
|
||
class MaterialPasswordInput extends StatefulWidget {
|
||
final TextEditingController? controller;
|
||
final String? labelText;
|
||
final String? hintText;
|
||
final IconData? prefixIcon;
|
||
final String? Function(String?)? validator;
|
||
final ValueChanged<String>? onChanged;
|
||
final ValueChanged<String>? onSubmitted;
|
||
final FocusNode? focusNode;
|
||
|
||
const MaterialPasswordInput({
|
||
super.key,
|
||
this.controller,
|
||
this.labelText,
|
||
this.hintText,
|
||
this.prefixIcon,
|
||
this.validator,
|
||
this.onChanged,
|
||
this.onSubmitted,
|
||
this.focusNode,
|
||
});
|
||
|
||
@override
|
||
State<MaterialPasswordInput> createState() => _MaterialPasswordInputState();
|
||
}
|
||
|
||
class _MaterialPasswordInputState extends State<MaterialPasswordInput> {
|
||
bool _obscureText = true;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
|
||
return MaterialInput(
|
||
controller: widget.controller,
|
||
labelText: widget.labelText,
|
||
hintText: widget.hintText,
|
||
prefixIcon: widget.prefixIcon ?? Icons.lock_outline,
|
||
obscureText: _obscureText,
|
||
validator: widget.validator,
|
||
onChanged: widget.onChanged,
|
||
onSubmitted: widget.onSubmitted,
|
||
focusNode: widget.focusNode,
|
||
suffixIcon: IconButton(
|
||
icon: Icon(
|
||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||
color: colorScheme.onSurfaceVariant,
|
||
size: 22,
|
||
),
|
||
onPressed: () {
|
||
setState(() {
|
||
_obscureText = !_obscureText;
|
||
});
|
||
},
|
||
splashRadius: 20,
|
||
),
|
||
);
|
||
}
|
||
}
|