import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../app/theme/app_colors.dart'; import '../../app/theme/app_spacing.dart'; /// 6 位 OTP 验证码输入组件 /// /// 特性: 自动跳转、支持粘贴、错误状态、自动聚焦 class OtpInput extends StatefulWidget { final int length; final ValueChanged onCompleted; final ValueChanged? onChanged; final bool hasError; final bool autofocus; const OtpInput({ super.key, this.length = 6, required this.onCompleted, this.onChanged, this.hasError = false, this.autofocus = true, }); @override State createState() => _OtpInputState(); } class _OtpInputState extends State { late List _controllers; late List _focusNodes; @override void initState() { super.initState(); _controllers = List.generate(widget.length, (_) => TextEditingController()); _focusNodes = List.generate(widget.length, (_) => FocusNode()); } @override void dispose() { for (final c in _controllers) { c.dispose(); } for (final n in _focusNodes) { n.dispose(); } super.dispose(); } String get _code => _controllers.map((c) => c.text).join(); void _onChanged(int index, String value) { // 粘贴完整验证码 if (value.length > 1) { final digits = value.replaceAll(RegExp(r'[^\d]'), ''); for (int i = 0; i < widget.length && i < digits.length; i++) { _controllers[i].text = digits[i]; } final focusIdx = digits.length.clamp(0, widget.length - 1); _focusNodes[focusIdx].requestFocus(); _notifyChange(); return; } if (value.isNotEmpty && index < widget.length - 1) { _focusNodes[index + 1].requestFocus(); } _notifyChange(); } void _onKeyEvent(int index, KeyEvent event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace && _controllers[index].text.isEmpty && index > 0) { _controllers[index - 1].clear(); _focusNodes[index - 1].requestFocus(); _notifyChange(); } } void _notifyChange() { final code = _code; widget.onChanged?.call(code); if (code.length == widget.length) { widget.onCompleted(code); } } void clear() { for (final c in _controllers) { c.clear(); } _focusNodes[0].requestFocus(); } @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.length, (i) { final isActive = _focusNodes[i].hasFocus; final hasFilled = _controllers[i].text.isNotEmpty; return Container( width: 48, height: 56, margin: EdgeInsets.symmetric(horizontal: i == 0 || i == widget.length - 1 ? 0 : 4), child: KeyboardListener( focusNode: FocusNode(), onKeyEvent: (event) => _onKeyEvent(i, event), child: TextField( controller: _controllers[i], focusNode: _focusNodes[i], autofocus: widget.autofocus && i == 0, textAlign: TextAlign.center, keyboardType: TextInputType.number, maxLength: 1, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(1), ], decoration: InputDecoration( counterText: '', contentPadding: EdgeInsets.zero, filled: true, fillColor: widget.hasError ? AppColors.errorLight : hasFilled ? AppColors.primarySurface : AppColors.gray50, enabledBorder: OutlineInputBorder( borderRadius: AppSpacing.borderRadiusMd, borderSide: BorderSide( color: widget.hasError ? AppColors.error : AppColors.border, width: 1.5, ), ), focusedBorder: OutlineInputBorder( borderRadius: AppSpacing.borderRadiusMd, borderSide: BorderSide( color: widget.hasError ? AppColors.error : AppColors.primary, width: 2, ), ), ), onChanged: (v) => _onChanged(i, v), ), ), ); }), ); } }