import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/core/widgets/app_text_field.dart'; import 'package:patbond_flutter/core/widgets/auth_scaffold.dart'; import 'package:patbond_flutter/core/widgets/inline_error_banner.dart'; import 'package:patbond_flutter/core/widgets/primary_button.dart'; import 'package:patbond_flutter/features/auth/auth_repository.dart'; enum _Field { username, phone, password, confirm } /// 注册页(12 号组装稿 §6)。字段按 ADR-004:用户名 + 手机号 + 密码 + 确认密码; /// 短信验证码行与第三方登录预留区一律不渲染。注册成功即建立会话直接进首页。 class RegisterPage extends StatefulWidget { const RegisterPage({required this.authRepository, super.key}); final AuthRepository authRepository; @override State createState() => _RegisterPageState(); } class _RegisterPageState extends State { static final _usernamePattern = RegExp(r'^[A-Za-z][A-Za-z0-9_]{2,19}$'); static final _phonePattern = RegExp(r'^1\d{10}$'); final _usernameCtrl = TextEditingController(); final _phoneCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); final _confirmCtrl = TextEditingController(); String? _usernameError; String? _phoneError; String? _passwordError; String? _confirmError; String? _formError; bool _submitting = false; @override void dispose() { _usernameCtrl.dispose(); _phoneCtrl.dispose(); _passwordCtrl.dispose(); _confirmCtrl.dispose(); super.dispose(); } String? _validateUsernameValue() { final value = _usernameCtrl.text.trim(); if (value.isEmpty) return '请输入用户名'; if (!_usernamePattern.hasMatch(value)) { return '用户名需 3–20 位,字母开头,可含数字和下划线'; } return null; } String? _validatePhoneValue() { final value = _phoneCtrl.text.trim(); if (value.isEmpty) return '请输入手机号'; if (!_phonePattern.hasMatch(value)) return '请输入正确的 11 位手机号'; return null; } String? _validatePasswordValue() { final value = _passwordCtrl.text; final lengthOk = value.length >= 8 && value.length <= 32; final hasLetter = value.contains(RegExp('[A-Za-z]')); final hasDigit = value.contains(RegExp('[0-9]')); if (!lengthOk || !hasLetter || !hasDigit) { return '密码需 8–32 位,且同时包含字母和数字'; } return null; } String? _validateConfirmValue() => _confirmCtrl.text == _passwordCtrl.text ? null : '两次输入的密码不一致'; void _validateOnBlur(_Field field) { if (!mounted) return; setState(() { switch (field) { case _Field.username: _usernameError = _validateUsernameValue(); case _Field.phone: _phoneError = _validatePhoneValue(); case _Field.password: _passwordError = _validatePasswordValue(); case _Field.confirm: _confirmError = _validateConfirmValue(); } }); } /// 字段变更清除本字段错误与横幅;密码变更时若确认密码已有值, /// 同步重校验一致性(组装稿 §6)。 void _clearError(_Field field) { setState(() { _formError = null; switch (field) { case _Field.username: _usernameError = null; case _Field.phone: _phoneError = null; case _Field.password: _passwordError = null; if (_confirmCtrl.text.isNotEmpty) { _confirmError = _validateConfirmValue(); } case _Field.confirm: _confirmError = null; } }); } void _showFormError(String message) { setState(() => _formError = message); SemanticsService.sendAnnouncement( View.of(context), message, TextDirection.ltr, ); } Future _submit() async { if (_submitting) return; final usernameError = _validateUsernameValue(); final phoneError = _validatePhoneValue(); final passwordError = _validatePasswordValue(); final confirmError = _validateConfirmValue(); if (usernameError != null || phoneError != null || passwordError != null || confirmError != null) { setState(() { _usernameError = usernameError; _phoneError = phoneError; _passwordError = passwordError; _confirmError = confirmError; }); return; } setState(() { _submitting = true; _formError = null; }); try { await widget.authRepository.register( username: _usernameCtrl.text.trim(), phone: '+86${_phoneCtrl.text.trim()}', password: _passwordCtrl.text, ); TextInput.finishAutofillContext(); if (!mounted) return; // 会话已建立,根路由切主壳;弹掉注册页露出新首页,不回登录页。 Navigator.of(context).popUntil((route) => route.isFirst); } on ApiBusinessException catch (error) { if (!mounted) return; switch (error.code) { case ApiCodes.usernameTaken: setState(() => _usernameError = '该用户名已被使用'); case ApiCodes.phoneTaken: setState(() => _phoneError = '该手机号已注册,可直接登录'); default: _showFormError('注册失败,请稍后重试'); } } on ApiRateLimitException { if (!mounted) return; _showFormError('尝试次数过多,请稍后再试'); } on ApiNetworkException { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('网络异常,请检查网络后重试'), action: SnackBarAction(label: '重试', onPressed: _submit), ), ); } on SessionExpiredException { // 注册请求不带会话,理论不可达。 } finally { if (mounted) setState(() => _submitting = false); } } Widget _blurValidated(_Field field, Widget child) { return Focus( onFocusChange: (hasFocus) { if (!hasFocus) _validateOnBlur(field); }, child: child, ); } @override Widget build(BuildContext context) { return AuthScaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, foregroundColor: AppColors.ink, ), child: AutofillGroup( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), Text('创建账号', style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 8), Text( '加入 Patbond,记录毛孩子的每一天', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 32), _blurValidated( _Field.username, AppTextField( label: '用户名', controller: _usernameCtrl, prefixIcon: Icons.person_outline_rounded, errorText: _usernameError, enabled: !_submitting, textInputAction: TextInputAction.next, autofillHints: const [AutofillHints.newUsername], onChanged: (_) => _clearError(_Field.username), ), ), const SizedBox(height: 16), _blurValidated( _Field.phone, AppTextField( label: '手机号', controller: _phoneCtrl, prefixIcon: Icons.phone_iphone_rounded, prefixText: '+86 ', errorText: _phoneError, enabled: !_submitting, keyboardType: TextInputType.phone, textInputAction: TextInputAction.next, autofillHints: const [AutofillHints.telephoneNumber], onChanged: (_) => _clearError(_Field.phone), ), ), const SizedBox(height: 16), _blurValidated( _Field.password, AppTextField( label: '密码', controller: _passwordCtrl, prefixIcon: Icons.lock_outline_rounded, obscurable: true, errorText: _passwordError, helperText: '密码 8–32 位,需包含字母和数字', enabled: !_submitting, textInputAction: TextInputAction.next, autofillHints: const [AutofillHints.newPassword], onChanged: (_) => _clearError(_Field.password), ), ), const SizedBox(height: 16), _blurValidated( _Field.confirm, AppTextField( label: '确认密码', controller: _confirmCtrl, prefixIcon: Icons.lock_outline_rounded, obscurable: true, errorText: _confirmError, enabled: !_submitting, textInputAction: TextInputAction.done, autofillHints: const [AutofillHints.newPassword], onChanged: (_) => _clearError(_Field.confirm), onSubmitted: (_) => _submit(), ), ), if (_formError != null) ...[ const SizedBox(height: 16), InlineErrorBanner(message: _formError!), ], const SizedBox(height: 32), PrimaryButton( label: '注册', isLoading: _submitting, onPressed: _submit, ), const SizedBox(height: 16), Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text( '已有账号?', style: TextStyle(color: AppColors.muted, fontSize: 14), ), TextButton( style: TextButton.styleFrom( foregroundColor: AppColors.primaryStrong, minimumSize: const Size(44, 44), padding: const EdgeInsets.symmetric(horizontal: 8), textStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), onPressed: _submitting ? null : () => Navigator.of(context).pop(), child: const Text('直接登录'), ), ], ), ), const SizedBox(height: 24), ], ), ), ); } }