import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../providers/auth_provider.dart'; import '../user/protocol_screen.dart'; import 'auth_widgets.dart'; import 'verify_code_screen.dart'; class RegisterScreen extends ConsumerStatefulWidget { const RegisterScreen({super.key}); @override ConsumerState createState() => _RegisterScreenState(); } class _RegisterScreenState extends ConsumerState { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _inviteController = TextEditingController(); final _emailFocusNode = FocusNode(); bool _obscurePassword = true; bool _agreed = false; bool _emailTouched = false; static final _emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$'); bool get _emailValid => _emailRegex.hasMatch(_emailController.text.trim()); bool get _has6to16 => _passwordController.text.length >= 6 && _passwordController.text.length <= 16; bool get _hasDigit => RegExp(r'[0-9]').hasMatch(_passwordController.text); bool get _hasLetter => RegExp(r'[a-zA-Z]').hasMatch(_passwordController.text); bool get _passwordValid => _has6to16 && _hasDigit && _hasLetter; @override void initState() { super.initState(); _emailFocusNode.addListener(() { if (!_emailFocusNode.hasFocus && _emailController.text.isNotEmpty) { setState(() => _emailTouched = true); } }); } @override void dispose() { _emailController.dispose(); _passwordController.dispose(); _inviteController.dispose(); _emailFocusNode.dispose(); super.dispose(); } /// 邮箱脱敏 String get _maskedEmail { final email = _emailController.text.trim(); if (!email.contains('@')) return email; final parts = email.split('@'); final name = parts[0]; if (name.length <= 1) return '$name***@${parts[1]}'; return '${name[0]}***@${parts[1]}'; } /// 点击注册 → 发送验证码成功后 → 跳转验证码页面 Future _handleRegisterTap() async { final notifier = ref.read(authProvider.notifier); final email = _emailController.text.trim(); if (email.isEmpty || !_passwordValid) return; final ok = await notifier.sendRegisterCode(email: email); if (!ok || !mounted) return; notifier.setPendingRegister( email: email, password: _passwordController.text, inviteCode: _inviteController.text.trim(), ); context.push( '/verify-code', extra: VerifyCodeArgs( email: email, password: _passwordController.text, maskedEmail: _maskedEmail, mode: VerifyCodeMode.register, showAuthenticator: false, codeSent: true, ), ); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final state = ref.watch(authProvider); final cs = Theme.of(context).colorScheme; ref.listen(authProvider, (prev, next) { if (next.errorMessage != null && next.errorMessage != prev?.errorMessage) { final code = next.errorMessage!; WidgetsBinding.instance.addPostFrameCallback((_) { if (!context.mounted) return; final l10n = AppLocalizations.of(context)!; showTopToast( context, message: resolveProviderError(code, l10n) ?? code, ); ref.read(authProvider.notifier).clearError(); }); } }); final canSubmit = _agreed && _emailValid && _passwordValid && !state.isLoading; final metCount = [_has6to16, _hasDigit, _hasLetter].where((m) => m).length; final pwdTyped = _passwordController.text.isNotEmpty; return Scaffold( appBar: AppBar( elevation: 0, leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), actions: [ TextButton( onPressed: () => context.pop(), child: Text( l10n.login, style: TextStyle(color: cs.onSurface, fontSize: 14), ), ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(24, 8, 24, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Logo + 标题(居中) Center( child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(32), child: Image.asset( 'assets/images/cv_logo.png', width: 64, height: 64, ), ), const SizedBox(height: 16), Text( l10n.welcomeJoin, style: TextStyle( color: cs.onSurface, fontSize: 22, fontWeight: FontWeight.w700, ), ), ], ), ), const SizedBox(height: 28), // 邮箱 Text(l10n.emailRegister, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13)), const SizedBox(height: 8), AuthField( controller: _emailController, hint: l10n.emailHint, keyboardType: TextInputType.emailAddress, maxLength: 100, focusNode: _emailFocusNode, onChanged: (_) => setState(() { _emailTouched = false; }), errorText: _emailTouched && !_emailValid ? l10n.emailError : null, semanticsLabel: 'register_input_email', ), const SizedBox(height: 20), // 密码 Text(l10n.loginPassword, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13)), const SizedBox(height: 8), AuthField( controller: _passwordController, hint: l10n.loginPasswordHint, obscure: _obscurePassword, maxLength: 16, onChanged: (_) => setState(() {}), semanticsLabel: 'register_input_password', // 只允许 ASCII 可打印字符(字母、数字、英文标点),禁止中文及特殊字符 inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[\x21-\x7E]')), ], suffixIcon: IconButton( onPressed: () => setState(() => _obscurePassword = !_obscurePassword), icon: Icon( _obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: cs.onSurface.withAlpha(153), size: 20, ), ), ), const SizedBox(height: 10), // 密码强度提示区域 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: cs.onSurface.withAlpha(100)), ), ), child: Column( children: [ Row( children: [ _StrengthChip(label: l10n.pwdCharsRule, met: _has6to16, typed: pwdTyped), const SizedBox(width: 8), _StrengthChip(label: l10n.pwdDigitRule, met: _hasDigit, typed: pwdTyped), const SizedBox(width: 8), _StrengthChip(label: l10n.pwdLetterRule, met: _hasLetter, typed: pwdTyped), ], ), const SizedBox(height: 8), Row( children: List.generate(3, (i) { Color barColor; if (!pwdTyped) { barColor = cs.onSurface.withAlpha(40); } else if (i < metCount) { barColor = metCount == 3 ? AppColors.rise : AppColors.fall; } else { barColor = cs.onSurface.withAlpha(40); } return Expanded( child: Container( height: 3, margin: EdgeInsets.only(right: i < 2 ? 6 : 0), decoration: BoxDecoration( color: barColor, borderRadius: BorderRadius.circular(2), ), ), ); }), ), ], ), ), const SizedBox(height: 20), // 邀请码 Text(l10n.inviteCode, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13)), const SizedBox(height: 8), AuthField( controller: _inviteController, hint: l10n.inviteCodeHint, maxLength: 10, semanticsLabel: 'register_input_invite_code', ), const SizedBox(height: 4), Text( l10n.inviteCodeTip, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 11), ), const SizedBox(height: 20), // 协议 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Semantics( label: 'register_checkbox_agree', button: true, enabled: true, onTap: () => setState(() => _agreed = !_agreed), child: GestureDetector( onTap: () => setState(() => _agreed = !_agreed), child: Container( width: 20, height: 20, margin: const EdgeInsets.only(top: 1), decoration: BoxDecoration( color: _agreed ? AppColors.brand : Colors.transparent, border: Border.all( color: _agreed ? AppColors.brand : cs.onSurface.withAlpha(100), width: 1.5, ), borderRadius: BorderRadius.circular(4), ), child: _agreed ? const Icon(Icons.check, size: 14, color: Colors.black) : null, ), ), ), const SizedBox(width: 8), Expanded( child: Text.rich( TextSpan( style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 12, ), children: [ TextSpan(text: l10n.agreePrefix), TextSpan( text: l10n.termsOfService, style: TextStyle(color: cs.onSurface), recognizer: TapGestureRecognizer() ..onTap = () => context.push('/protocol', extra: ProtocolArgs(title: l10n.termsOfService)), ), const TextSpan(text: '、'), TextSpan( text: l10n.privacyPolicy, style: TextStyle(color: cs.onSurface), recognizer: TapGestureRecognizer() ..onTap = () => context.push('/protocol', extra: ProtocolArgs( title: l10n.privacyPolicy, categoryCode: 'PRIVACY', )), ), ], ), ), ), ], ), const SizedBox(height: 32), // 注册按钮 SafeArea( top: false, child: Semantics( label: 'register_btn_submit', button: true, enabled: canSubmit, onTap: canSubmit ? _handleRegisterTap : null, child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: canSubmit ? _handleRegisterTap : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, disabledBackgroundColor: AppColors.brand.withAlpha(80), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), ), child: state.isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black), ) : Text( l10n.registerAccount, style: TextStyle( color: canSubmit ? Colors.black : Colors.black.withAlpha(153), fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), ), const SizedBox(height: 24), ], ), ), ); } } // ── 密码强度提示芯片 ────────────────────────────────────────── class _StrengthChip extends StatelessWidget { const _StrengthChip({required this.label, required this.met, this.typed = false}); final String label; final bool met; final bool typed; @override Widget build(BuildContext context) { final Color color; final IconData icon; if (!typed) { color = Theme.of(context).colorScheme.onSurface.withAlpha(120); icon = Icons.circle; } else if (met) { color = AppColors.rise; icon = Icons.check; } else { color = AppColors.fall; icon = Icons.close; } return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: color.withAlpha(80), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: icon == Icons.circle ? 6 : 12, color: color), const SizedBox(width: 4), Text( label, style: TextStyle(color: color, fontSize: 11), ), ], ), ); } }