import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_spacing.dart'; import '../../../../shared/widgets/genex_button.dart'; import '../../../../shared/widgets/countdown_button.dart'; import '../../../../core/services/auth_service.dart'; import '../../../../app/i18n/app_localizations.dart'; /// A1. 手机号注册页 /// /// 3步流程: 输入手机号+验证码 → 设置密码 → 完成注册 class RegisterPage extends StatefulWidget { final bool isEmail; const RegisterPage({super.key, this.isEmail = false}); @override State createState() => _RegisterPageState(); } class _RegisterPageState extends State { final _accountController = TextEditingController(); final _codeController = TextEditingController(); final _passwordController = TextEditingController(); final _referralCodeController = TextEditingController(); bool _obscurePassword = true; bool _agreeTerms = false; bool _loading = false; String? _errorMessage; // null=未验证, true=有效, false=无效 bool? _referralCodeValid; final _authService = AuthService.instance; @override void initState() { super.initState(); _passwordController.addListener(() => setState(() {})); } @override void dispose() { _accountController.dispose(); _codeController.dispose(); _passwordController.dispose(); _referralCodeController.dispose(); super.dispose(); } Future _validateReferralCode(String code) async { if (code.isEmpty) { setState(() => _referralCodeValid = null); return; } try { final resp = await _authService.validateReferralCode(code); setState(() => _referralCodeValid = resp); } catch (_) { setState(() => _referralCodeValid = false); } } String _extractError(DioException e) { final data = e.response?.data; if (data is Map && data['message'] != null) { return data['message'].toString(); } return context.t('login.networkError'); } /* ── 发送注册验证码 ── */ Future _handleSendCode() async { final phone = _accountController.text.trim(); if (phone.isEmpty) { setState(() => _errorMessage = context.t('register.errorPhoneRequired')); throw Exception('phone required'); } setState(() => _errorMessage = null); try { await _authService.sendSmsCode(phone, SmsCodeType.register); } on DioException catch (e) { setState(() => _errorMessage = _extractError(e)); rethrow; } } /* ── 提交注册 ── */ Future _handleRegister() async { final phone = _accountController.text.trim(); final code = _codeController.text.trim(); final password = _passwordController.text; if (phone.isEmpty) { setState(() => _errorMessage = context.t('register.errorPhoneRequired')); return; } if (code.length != 6) { setState(() => _errorMessage = context.t('register.errorCodeInvalid')); return; } if (password.length < 8 || !RegExp(r'[a-zA-Z]').hasMatch(password) || !RegExp(r'\d').hasMatch(password)) { setState(() => _errorMessage = context.t('register.errorPasswordWeak')); return; } if (!_agreeTerms) { setState(() => _errorMessage = context.t('register.errorTermsRequired')); return; } setState(() { _loading = true; _errorMessage = null; }); try { final referralCode = _referralCodeController.text.trim(); await _authService.register( phone: phone, smsCode: code, password: password, referralCode: referralCode.isNotEmpty ? referralCode : null, ); if (mounted) Navigator.pushReplacementNamed(context, '/main'); } on DioException catch (e) { setState(() => _errorMessage = _extractError(e)); } catch (e) { setState(() => _errorMessage = e.toString()); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), onPressed: () => Navigator.of(context).pop(), ), ), body: SafeArea( child: SingleChildScrollView( padding: AppSpacing.pagePadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), Text(context.t('register.title'), style: AppTypography.displayMedium), const SizedBox(height: 8), Text( widget.isEmail ? context.t('register.emailSubtitle') : context.t('register.phoneSubtitle'), style: AppTypography.bodyLarge.copyWith(color: AppColors.textSecondary), ), const SizedBox(height: 40), // Step indicator _buildStepIndicator(), const SizedBox(height: 32), // Error message if (_errorMessage != null) ...[ Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: AppColors.errorLight, borderRadius: AppSpacing.borderRadiusSm, ), child: Row( children: [ const Icon(Icons.error_outline, color: AppColors.error, size: 18), const SizedBox(width: 8), Expanded( child: Text( _errorMessage!, style: AppTypography.bodySmall.copyWith(color: AppColors.error), ), ), ], ), ), const SizedBox(height: 16), ], // Step 1: Phone + Code Text( widget.isEmail ? context.t('register.email') : context.t('register.phone'), style: AppTypography.labelMedium, ), const SizedBox(height: 8), TextField( controller: _accountController, keyboardType: widget.isEmail ? TextInputType.emailAddress : TextInputType.phone, decoration: InputDecoration( hintText: widget.isEmail ? context.t('register.emailHint') : context.t('register.phoneHint'), prefixIcon: Icon( widget.isEmail ? Icons.email_outlined : Icons.phone_android_rounded, color: AppColors.textTertiary, ), ), ), const SizedBox(height: 20), Text(context.t('register.code'), style: AppTypography.labelMedium), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _codeController, keyboardType: TextInputType.number, maxLength: 6, decoration: InputDecoration( hintText: context.t('register.codeHint'), counterText: '', prefixIcon: const Icon(Icons.shield_outlined, color: AppColors.textTertiary), ), ), ), const SizedBox(width: 12), CountdownButton( label: context.t('register.getCode'), onPressed: _handleSendCode, ), ], ), const SizedBox(height: 20), // Step 2: Password Text(context.t('register.setPassword'), style: AppTypography.labelMedium), const SizedBox(height: 8), TextField( controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( hintText: context.t('register.passwordHint'), prefixIcon: const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: AppColors.textTertiary, size: 20, ), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), ), ), const SizedBox(height: 8), _buildPasswordStrength(), const SizedBox(height: 20), // Referral code (optional) Text(context.t('register.referralCode'), style: AppTypography.labelMedium), const SizedBox(height: 8), TextField( controller: _referralCodeController, textCapitalization: TextCapitalization.characters, onChanged: (v) { setState(() => _referralCodeValid = null); if (v.length >= 6) _validateReferralCode(v.trim()); }, decoration: InputDecoration( hintText: context.t('register.referralCodeHint'), prefixIcon: const Icon(Icons.card_giftcard_rounded, color: AppColors.textTertiary), suffixIcon: _referralCodeValid == null ? null : Icon( _referralCodeValid! ? Icons.check_circle_rounded : Icons.cancel_rounded, color: _referralCodeValid! ? AppColors.success : AppColors.error, size: 20, ), helperText: _referralCodeValid == null ? null : _referralCodeValid! ? context.t('register.referralCodeValid') : context.t('register.referralCodeInvalid'), helperStyle: TextStyle( color: _referralCodeValid == true ? AppColors.success : AppColors.error, fontSize: 12, ), ), ), const SizedBox(height: 24), // Step 3: Terms + Submit Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 20, height: 20, child: Checkbox( value: _agreeTerms, onChanged: (v) => setState(() => _agreeTerms = v ?? false), activeColor: AppColors.primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), ), const SizedBox(width: 8), Expanded( child: GestureDetector( onTap: () => setState(() => _agreeTerms = !_agreeTerms), child: RichText( text: TextSpan( style: AppTypography.bodySmall, children: [ TextSpan(text: '${context.t('register.agreement')} '), TextSpan( text: context.t('register.userAgreement'), style: AppTypography.bodySmall.copyWith(color: AppColors.primary), ), TextSpan(text: ' ${context.t('register.and')} '), TextSpan( text: context.t('register.privacyPolicy'), style: AppTypography.bodySmall.copyWith(color: AppColors.primary), ), ], ), ), ), ), ], ), const SizedBox(height: 32), GenexButton( label: context.t('register.submit'), isLoading: _loading, onPressed: _agreeTerms ? _handleRegister : null, ), // Login link const SizedBox(height: 24), Center( child: GestureDetector( onTap: () => Navigator.pushReplacementNamed(context, '/login'), child: RichText( text: TextSpan( style: AppTypography.bodySmall.copyWith(color: AppColors.textSecondary), children: [ TextSpan(text: context.t('register.hasAccount')), TextSpan( text: context.t('register.loginNow'), style: const TextStyle(color: AppColors.primary, fontWeight: FontWeight.w600), ), ], ), ), ), ), const SizedBox(height: 40), ], ), ), ), ); } Widget _buildStepIndicator() { final hasCode = _codeController.text.length == 6; final hasPassword = _passwordController.text.length >= 8; return Row( children: [ _buildStep(1, context.t('register.stepVerify'), true), _buildStepLine(hasCode), _buildStep(2, context.t('register.stepPassword'), hasCode), _buildStepLine(hasCode && hasPassword), _buildStep(3, context.t('register.stepDone'), hasCode && hasPassword), ], ); } Widget _buildStep(int number, String label, bool active) { return Column( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: active ? AppColors.primary : AppColors.gray200, shape: BoxShape.circle, ), child: Center( child: Text( '$number', style: TextStyle( color: active ? Colors.white : AppColors.textTertiary, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(height: 4), Text( label, style: AppTypography.caption.copyWith( color: active ? AppColors.primary : AppColors.textTertiary, ), ), ], ); } Widget _buildStepLine(bool active) { return Expanded( child: Container( height: 2, margin: const EdgeInsets.only(bottom: 18), color: active ? AppColors.primary : AppColors.gray200, ), ); } Widget _buildPasswordStrength() { final password = _passwordController.text; final hasLength = password.length >= 8; final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password); final hasDigit = RegExp(r'\d').hasMatch(password); return Row( children: [ _buildCheck(context.t('register.rule8chars'), hasLength), const SizedBox(width: 16), _buildCheck(context.t('register.ruleLetter'), hasLetter), const SizedBox(width: 16), _buildCheck(context.t('register.ruleNumber'), hasDigit), ], ); } Widget _buildCheck(String label, bool passed) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon( passed ? Icons.check_circle_rounded : Icons.circle_outlined, size: 14, color: passed ? AppColors.success : AppColors.textTertiary, ), const SizedBox(width: 4), Text( label, style: AppTypography.caption.copyWith( color: passed ? AppColors.success : AppColors.textTertiary, ), ), ], ); } }