feat: 改用 Material Design 3 标准输入框
- 创建 MaterialInput 和 MaterialPasswordInput 组件 - 移除 shadcn_ui 依赖,使用原生 Material 组件 - 添加圆角边框 + Focus 效果 - 添加浮动标签动画 - 改进登录和注册页面视觉体验 - 统一设计语言(Material Design 3)
This commit is contained in:
230
flutter_monisuo/lib/ui/components/material_input.dart
Normal file
230
flutter_monisuo/lib/ui/components/material_input.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user