2026-04-08 11:11:43 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
2026-04-08 11:15:13 +08:00
|
|
|
|
import '../../core/theme/app_color_scheme.dart';
|
|
|
|
|
|
import '../../core/theme/app_spacing.dart';
|
|
|
|
|
|
import '../../core/theme/app_theme.dart';
|
|
|
|
|
|
import '../../core/theme/app_theme_extension.dart';
|
2026-04-08 11:11:43 +08:00
|
|
|
|
|
|
|
|
|
|
/// 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,
|
2026-04-08 11:49:42 +08:00
|
|
|
|
style: AppTextStyles.bodyLarge(context).copyWith(
|
2026-04-08 11:11:43 +08:00
|
|
|
|
color: enabled
|
|
|
|
|
|
? colorScheme.onSurface
|
|
|
|
|
|
: colorScheme.onSurface.withValues(alpha: 0.5),
|
2026-04-08 11:49:42 +08:00
|
|
|
|
fontSize: 16, // 统一字体大小
|
2026-04-08 11:11:43 +08:00
|
|
|
|
),
|
|
|
|
|
|
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,
|
|
|
|
|
|
|
2026-04-08 11:49:42 +08:00
|
|
|
|
// 填充颜色(移除,不需要)
|
|
|
|
|
|
filled: false,
|
2026-04-08 11:11:43 +08:00
|
|
|
|
|
2026-04-08 11:49:42 +08:00
|
|
|
|
// 内容内边距(统一使用 16px)
|
2026-04-08 11:11:43 +08:00
|
|
|
|
contentPadding: const EdgeInsets.symmetric(
|
2026-04-08 11:49:42 +08:00
|
|
|
|
horizontal: 16,
|
|
|
|
|
|
vertical: 16,
|
2026-04-08 11:11:43 +08:00
|
|
|
|
),
|
|
|
|
|
|
|
2026-04-08 11:49:42 +08:00
|
|
|
|
// Material Design 3 边框样式(统一 12px 圆角)
|
|
|
|
|
|
border: _buildBorder(borderColor, 12),
|
|
|
|
|
|
enabledBorder: _buildBorder(borderColor, 12),
|
|
|
|
|
|
focusedBorder: _buildBorder(primaryColor, 12, width: 2.0),
|
|
|
|
|
|
errorBorder: _buildBorder(colorScheme.error, 12),
|
|
|
|
|
|
focusedErrorBorder: _buildBorder(colorScheme.error, 12, width: 2.0),
|
2026-04-08 11:11:43 +08:00
|
|
|
|
disabledBorder: _buildBorder(
|
|
|
|
|
|
borderColor.withValues(alpha: 0.3),
|
2026-04-08 11:49:42 +08:00
|
|
|
|
12,
|
2026-04-08 11:11:43 +08:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// 错误文本样式
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|