import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/api/auth/api.dart'; import 'package:new_learn/core/network/dio_client.dart'; import 'package:new_learn/core/components/confirm_box.dart'; /// ============================================================ /// 【RegisterPage】注册页 —— 对照 Vue 版 pages/login/register.vue /// /// 一、表单字段(3 个,带校验): /// 账号 6-16 位字母或数字 → /^[a-zA-Z0-9]{6,16}$/ /// 密码 6-16 位字母或数字 → /^[a-zA-Z0-9]{6,16}$/ /// 确认密码 必须与密码一致 /// /// 二、注册流程: /// isFormValid → 协议勾选 → 调 UserApi.register({account, password}) /// → 成功 Toast "注册成功" → 1 秒后返回登录页 /// /// 三、与 Vue 的对应关系: /// validateField / validateAll → Flutter 版字段级校验(_validateXxx) /// isFormValid computed → Flutter 版 _isFormValid getter /// debounce 防重复提交 → Flutter 用 _loading 标记防连点 /// 密码 MD5 → UserApi.register 内部已处理 /// /// 四、UI 风格: /// 输入框对齐登录页(灰底 #F9F9F9 + 圆角 24.r),顶部 AppBar 标题"注册账号"。 /// ============================================================ /// 账号/密码校验正则:6-16 位字母或数字(对齐 Vue register.vue 的 validateField) final _kAccountRegex = RegExp(r'^[a-zA-Z0-9]{6,16}$'); class RegisterPage extends ConsumerStatefulWidget { const RegisterPage({super.key}); @override ConsumerState createState() => _RegisterPageState(); } class _RegisterPageState extends ConsumerState { // ---------- 表单控制器 ---------- final TextEditingController _accountController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _confirmPasswordController = TextEditingController(); bool _obscurePassword = true; bool _obscureConfirmPassword = true; bool _hasAgreed = false; bool _loading = false; // ---------- 错误信息(字段级,与 Vue errors 对齐) ---------- String _accountError = ''; String _passwordError = ''; String _confirmPasswordError = ''; @override void dispose() { _accountController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } // ==================== 校验(对齐 Vue validateField) ==================== String _validateAccount() { final v = _accountController.text.trim(); if (v.isEmpty) return '请填写账号'; if (!_kAccountRegex.hasMatch(v)) return '6-16 位字母或数字'; return ''; } String _validatePassword() { final v = _passwordController.text.trim(); if (v.isEmpty) return '请填写密码'; if (!_kAccountRegex.hasMatch(v)) return '6-16 位字母或数字'; // 密码变化时重校验确认密码 if (_confirmPasswordController.text.isNotEmpty) { _confirmPasswordError = _confirmPasswordController.text == v ? '' : '两次输入的密码不一致'; } return ''; } String _validateConfirmPassword() { final v = _confirmPasswordController.text; if (v.isEmpty) return '请再次输入密码'; if (v != _passwordController.text) return '两次输入的密码不一致'; return ''; } /// 表单是否完整有效(对齐 Vue isFormValid computed) bool get _isFormValid { return _kAccountRegex.hasMatch(_accountController.text.trim()) && _kAccountRegex.hasMatch(_passwordController.text.trim()) && _confirmPasswordController.text.isNotEmpty && _confirmPasswordController.text == _passwordController.text; } // ==================== 注册逻辑 ==================== Future _handleRegister() async { if (_loading) return; // 全量校验 _accountError = _validateAccount(); _passwordError = _validatePassword(); _confirmPasswordError = _validateConfirmPassword(); setState(() {}); if (!_isFormValid) return; // 未勾选协议 → 弹确认框(对齐 Vue register.vue 的协议弹窗) if (!_hasAgreed) { final agreed = await showConfirmBox( context, title: '用户协议及隐私条款', content: '感谢您选择AI爱学蝶变!我们非常重视您的个人信息和隐私保护。' '为了更好地保障您的个人权益,在您使用我们的产品前请务必审慎阅读' '《隐私政策》和《用户协议》内的所有条款。同意并继续开始使用我们的产品和服务!', cancelText: '不同意', confirmText: '同意并继续', ); if (!agreed || !mounted) return; setState(() => _hasAgreed = true); } setState(() => _loading = true); try { await UserApi.register( account: _accountController.text.trim(), password: _passwordController.text, ); if (!mounted) return; _toast('注册成功'); // 1 秒后回到登录页(context.go 替换当前栈,最稳不依赖 canPop) Future.delayed(const Duration(seconds: 1), () { if (mounted) context.go('/login'); }); } on ApiException catch (e) { if (mounted) _toast(e.message); } catch (e) { if (mounted) _toast('注册失败,请稍后重试'); } finally { if (mounted) setState(() => _loading = false); } } void _toast(String msg) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(msg), duration: const Duration(seconds: 2)), ); } // ==================== 导航 ==================== void _goToAgreement() => context.push('/user-agreement'); void _goToPrivacy() => context.push('/privacy-policy'); // ==================== UI ==================== @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, elevation: 0, title: const Text('注册账号', style: TextStyle(color: Color(0xFF323233))), iconTheme: const IconThemeData(color: Color(0xFF323233)), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.canPop() ? context.pop() : context.go('/login'), ), ), body: SafeArea( child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 25.w), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox(height: 20.h), // 账号 _buildField( label: '账号', controller: _accountController, hintText: '请输入您的账号', errorText: _accountError, onChanged: (_) => setState(() => _accountError = _validateAccount()), ), SizedBox(height: 12.h), // 密码 _buildField( label: '密码', controller: _passwordController, hintText: '请输入您的密码', obscureText: _obscurePassword, errorText: _passwordError, onChanged: (_) => setState(() { _passwordError = _validatePassword(); }), onToggleObscure: () => setState(() => _obscurePassword = !_obscurePassword), ), SizedBox(height: 12.h), // 确认密码 _buildField( label: '确认密码', controller: _confirmPasswordController, hintText: '请再次输入密码', obscureText: _obscureConfirmPassword, errorText: _confirmPasswordError, onChanged: (_) => setState( () => _confirmPasswordError = _validateConfirmPassword(), ), onToggleObscure: () => setState( () => _obscureConfirmPassword = !_obscureConfirmPassword, ), ), SizedBox(height: 25.h), // 协议勾选 _buildAgreementRow(), SizedBox(height: 20.h), // 注册按钮 _buildRegisterButton(), SizedBox(height: 20.h), // 已有账号,去登录 _buildFooterLink(), ], ), ), ), ); } /// 通用输入字段(带 label + 灰底圆角 + 错误提示) Widget _buildField({ required String label, required TextEditingController controller, required String hintText, required String errorText, bool obscureText = false, ValueChanged? onChanged, VoidCallback? onToggleObscure, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 输入框容器(灰底 #F9F9F9 + 圆角 24.r,对齐登录页) Container( height: 48.h, padding: EdgeInsets.symmetric(horizontal: 16.w), decoration: BoxDecoration( color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(24.r), ), child: Row( children: [ // label 文字(Vue register-page__label 140rpx 宽) SizedBox( width: 70.w, // 140rpx → .w child: Text( label, style: TextStyle( fontSize: 14.sp, color: const Color(0xFF323233), ), ), ), Expanded( child: TextField( controller: controller, obscureText: obscureText, onChanged: onChanged, style: TextStyle( fontSize: 16.sp, color: const Color(0xFF323233), ), decoration: InputDecoration( isCollapsed: true, border: InputBorder.none, hintText: hintText, hintStyle: TextStyle( fontSize: 14.sp, color: const Color(0xFFC0C4CC), ), ), ), ), if (onToggleObscure != null) GestureDetector( onTap: onToggleObscure, child: Icon( obscureText ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: const Color(0xFFC0C4CC), size: 24, ), ), ], ), ), // 错误提示:固定高度占位,有错误时显示文字,无错误时空白 → 输入框位置不动 SizedBox( height: 22.h, child: errorText.isNotEmpty ? Padding( padding: EdgeInsets.only(left: 16.w, top: 6.h), child: Text( errorText, style: TextStyle( fontSize: 12.sp, color: const Color(0xFFEE0A24), ), ), ) : null, ), ], ); } Widget _buildAgreementRow() { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ InkWell( onTap: () => setState(() => _hasAgreed = !_hasAgreed), child: Icon( _hasAgreed ? Icons.check_circle_rounded : Icons.radio_button_unchecked, size: 16.w, color: _hasAgreed ? const Color(0xFF3D7DF5) : const Color(0xFFC0C4CC), ), ), SizedBox(width: 6.w), Expanded( child: RichText( text: TextSpan( style: TextStyle(fontSize: 14.sp, color: const Color(0xFF323233)), children: [ const TextSpan(text: '我已阅读并同意'), TextSpan( text: '《用户协议》', style: const TextStyle(color: Color(0xFF3D7DF5)), recognizer: TapGestureRecognizer()..onTap = _goToAgreement, ), const TextSpan(text: '和'), TextSpan( text: '《隐私政策》', style: const TextStyle(color: Color(0xFF3D7DF5)), recognizer: TapGestureRecognizer()..onTap = _goToPrivacy, ), ], ), ), ), ], ); } Widget _buildRegisterButton() { return SizedBox( width: double.infinity, height: 48.h, child: FilledButton( onPressed: _loading ? null : _handleRegister, style: FilledButton.styleFrom( backgroundColor: const Color(0xFF3D7DF5), disabledBackgroundColor: _isFormValid ? const Color(0xFF3D7DF5).withValues(alpha: 0.6) : const Color(0xFFC0C4CC), // 表单无效时整体灰色,对齐 Vue btn--disabled shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.r), ), ), child: _loading ? SizedBox( width: 18.w, height: 18.w, child: const CircularProgressIndicator( strokeWidth: 3, color: Colors.white, ), ) : Text( '注册', style: TextStyle(fontSize: 18.sp, color: Colors.white), ), ), ); } Widget _buildFooterLink() { return Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: () => context.canPop() ? context.pop() : context.go('/login'), child: Text( '已有账号,去登录', style: TextStyle(fontSize: 14.sp, color: const Color(0xFF3D7DF5)), ), ), ); } }