From 65f3e75f599125d42296d4606fe3119d767cca18 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 20 Dec 2025 20:13:09 -0800 Subject: [PATCH] =?UTF-8?q?feat(mobile-app):=20=E6=B7=BB=E5=8A=A0=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E5=8F=B7+=E5=AF=86=E7=A0=81=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=92=8C=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能: - 创建 PhoneLoginPage 用于账号恢复功能 - 实现手机号和密码输入界面 - 添加输入验证(手机号格式、密码长度) - 添加密码可见性切换 - 添加登录按钮和加载状态 - 配置 phoneLogin 路由到 app_router - 添加 RouteNames.phoneLogin 常量 UI设计: - 深色渐变背景(与其他认证页面一致) - 返回按钮 - 手机号和密码输入框 - 错误提示区域 - 登录按钮(带加载状态) - 注册提示链接 待实现: - 后端手机号+密码登录 API - 登录成功后的token保存和状态更新 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../presentation/pages/phone_login_page.dart | 504 ++++++++++++++++++ .../mobile-app/lib/routes/app_router.dart | 10 + .../mobile-app/lib/routes/route_names.dart | 1 + 3 files changed, 515 insertions(+) create mode 100644 frontend/mobile-app/lib/features/auth/presentation/pages/phone_login_page.dart diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/phone_login_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/phone_login_page.dart new file mode 100644 index 00000000..2b0bb710 --- /dev/null +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/phone_login_page.dart @@ -0,0 +1,504 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/account_service.dart'; +import '../../../../core/storage/secure_storage.dart'; +import '../../../../core/storage/storage_keys.dart'; +import '../../../../routes/route_paths.dart'; +import '../providers/auth_provider.dart'; + +/// 手机号+密码登录页面 +/// 用于"恢复账号"功能 +class PhoneLoginPage extends ConsumerStatefulWidget { + const PhoneLoginPage({super.key}); + + @override + ConsumerState createState() => _PhoneLoginPageState(); +} + +class _PhoneLoginPageState extends ConsumerState { + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final FocusNode _phoneFocusNode = FocusNode(); + final FocusNode _passwordFocusNode = FocusNode(); + + bool _isLoggingIn = false; + String? _errorMessage; + bool _obscurePassword = true; // 是否隐藏密码 + + @override + void dispose() { + _phoneController.dispose(); + _passwordController.dispose(); + _phoneFocusNode.dispose(); + _passwordFocusNode.dispose(); + super.dispose(); + } + + /// 验证手机号格式 + bool _isValidPhoneNumber(String phone) { + // 中国大陆手机号:1开头,第二位3-9,共11位 + final regex = RegExp(r'^1[3-9]\d{9}$'); + return regex.hasMatch(phone); + } + + /// 验证输入 + String? _validateInputs() { + final phone = _phoneController.text.trim(); + final password = _passwordController.text; + + if (phone.isEmpty) { + return '请输入手机号'; + } + + if (!_isValidPhoneNumber(phone)) { + return '请输入正确的手机号'; + } + + if (password.isEmpty) { + return '请输入密码'; + } + + if (password.length < 6) { + return '密码至少6位'; + } + + return null; + } + + /// 执行登录 + Future _login() async { + // 验证输入 + final validationError = _validateInputs(); + if (validationError != null) { + setState(() { + _errorMessage = validationError; + }); + return; + } + + setState(() { + _isLoggingIn = true; + _errorMessage = null; + }); + + try { + final phone = _phoneController.text.trim(); + final password = _passwordController.text; + + debugPrint('[PhoneLoginPage] 开始登录 - 手机号: $phone'); + + // 调用登录 API(需要在 AccountService 中添加) + // TODO: 实现手机号+密码登录 API + // final response = await accountService.loginWithPassword(phone, password); + + // 暂时模拟登录失败 + throw Exception('手机号+密码登录 API 尚未实现'); + + // 登录成功后的处理: + // 1. 保存 access token 和 refresh token + // 2. 保存用户信息(userId, accountSequence, referralCode) + // 3. 检查钱包状态 + // 4. 跳转到主页 + + // if (mounted) { + // // 更新认证状态 + // await ref.read(authProvider.notifier).checkAuthStatus(); + // + // // 跳转到主页(龙虎榜) + // context.go(RoutePaths.ranking); + // } + } catch (e) { + debugPrint('[PhoneLoginPage] 登录失败: $e'); + setState(() { + _errorMessage = '登录失败: $e'; + }); + } finally { + if (mounted) { + setState(() { + _isLoggingIn = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF1A1A2E), + Color(0xFF16213E), + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 顶部导航栏 + _buildAppBar(), + // 表单内容 + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 40.h), + // 标题 + Text( + '恢复账号', + style: TextStyle( + fontSize: 32.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 8.h), + Text( + '使用手机号和密码登录', + style: TextStyle( + fontSize: 16.sp, + color: Colors.white.withOpacity(0.6), + ), + ), + SizedBox(height: 48.h), + // 手机号输入框 + _buildPhoneInput(), + SizedBox(height: 16.h), + // 密码输入框 + _buildPasswordInput(), + // 错误提示 + if (_errorMessage != null) ...[ + SizedBox(height: 16.h), + _buildErrorMessage(), + ], + SizedBox(height: 32.h), + // 登录按钮 + _buildLoginButton(), + SizedBox(height: 24.h), + // 分割线和提示 + _buildDivider(), + SizedBox(height: 24.h), + // 注册提示 + _buildRegisterHint(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// 构建顶部导航栏 + Widget _buildAppBar() { + return Container( + height: 56.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + Icons.arrow_back, + color: Colors.white, + size: 20.sp, + ), + ), + ), + ], + ), + ); + } + + /// 构建手机号输入框 + Widget _buildPhoneInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '手机号', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 1, + ), + ), + child: TextField( + controller: _phoneController, + focusNode: _phoneFocusNode, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(11), + ], + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + decoration: InputDecoration( + hintText: '请输入手机号', + hintStyle: TextStyle( + fontSize: 16.sp, + color: Colors.white.withOpacity(0.4), + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 14.h, + ), + prefixIcon: Icon( + Icons.phone_android, + color: Colors.white.withOpacity(0.6), + size: 20.sp, + ), + ), + onChanged: (_) { + if (_errorMessage != null) { + setState(() => _errorMessage = null); + } + }, + ), + ), + ], + ); + } + + /// 构建密码输入框 + Widget _buildPasswordInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '密码', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 1, + ), + ), + child: TextField( + controller: _passwordController, + focusNode: _passwordFocusNode, + obscureText: _obscurePassword, + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + decoration: InputDecoration( + hintText: '请输入密码', + hintStyle: TextStyle( + fontSize: 16.sp, + color: Colors.white.withOpacity(0.4), + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 14.h, + ), + prefixIcon: Icon( + Icons.lock_outline, + color: Colors.white.withOpacity(0.6), + size: 20.sp, + ), + suffixIcon: GestureDetector( + onTap: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + child: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white.withOpacity(0.6), + size: 20.sp, + ), + ), + ), + onChanged: (_) { + if (_errorMessage != null) { + setState(() => _errorMessage = null); + } + }, + onSubmitted: (_) => _login(), + ), + ), + ], + ); + } + + /// 构建错误提示 + Widget _buildErrorMessage() { + return Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: Colors.red.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red, + size: 16.sp, + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle( + fontSize: 14.sp, + color: Colors.red, + ), + ), + ), + ], + ), + ); + } + + /// 构建登录按钮 + Widget _buildLoginButton() { + final canLogin = !_isLoggingIn && + _phoneController.text.trim().isNotEmpty && + _passwordController.text.isNotEmpty; + + return SizedBox( + width: double.infinity, + height: 50.h, + child: ElevatedButton( + onPressed: canLogin ? _login : null, + style: ElevatedButton.styleFrom( + backgroundColor: canLogin + ? const Color(0xFFD4AF37) + : Colors.white.withOpacity(0.2), + disabledBackgroundColor: Colors.white.withOpacity(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: _isLoggingIn + ? SizedBox( + width: 20.w, + height: 20.h, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + '登录', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: canLogin ? Colors.white : Colors.white.withOpacity(0.4), + ), + ), + ), + ); + } + + /// 构建分割线 + Widget _buildDivider() { + return Row( + children: [ + Expanded( + child: Divider( + color: Colors.white.withOpacity(0.2), + thickness: 1, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Text( + '或', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.5), + ), + ), + ), + Expanded( + child: Divider( + color: Colors.white.withOpacity(0.2), + thickness: 1, + ), + ), + ], + ); + } + + /// 构建注册提示 + Widget _buildRegisterHint() { + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '还没有账号?', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.6), + ), + ), + SizedBox(width: 4.w), + GestureDetector( + onTap: () { + // 返回向导页 + context.go(RoutePaths.guide); + }, + child: Text( + '立即注册', + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFFD4AF37), + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index ce2e96db..19ce6173 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -10,6 +10,7 @@ import '../features/auth/presentation/pages/verify_mnemonic_page.dart'; import '../features/auth/presentation/pages/wallet_created_page.dart'; import '../features/auth/presentation/pages/import_mnemonic_page.dart'; import '../features/auth/presentation/pages/phone_register_page.dart'; +import '../features/auth/presentation/pages/phone_login_page.dart'; import '../features/auth/presentation/pages/sms_verify_page.dart'; import '../features/auth/presentation/pages/set_password_page.dart'; import '../features/home/presentation/pages/home_shell_page.dart'; @@ -145,6 +146,15 @@ final appRouterProvider = Provider((ref) { }, ), + // Phone Login (手机号+密码登录) + GoRoute( + path: RoutePaths.phoneLogin, + name: RouteNames.phoneLogin, + builder: (context, state) { + return const PhoneLoginPage(); + }, + ), + // SMS Verify (短信验证码) GoRoute( path: RoutePaths.smsVerify, diff --git a/frontend/mobile-app/lib/routes/route_names.dart b/frontend/mobile-app/lib/routes/route_names.dart index 90c5878c..ff036a8c 100644 --- a/frontend/mobile-app/lib/routes/route_names.dart +++ b/frontend/mobile-app/lib/routes/route_names.dart @@ -12,6 +12,7 @@ class RouteNames { static const importWallet = 'import-wallet'; static const importMnemonic = 'import-mnemonic'; static const phoneRegister = 'phone-register'; + static const phoneLogin = 'phone-login'; static const smsVerify = 'sms-verify'; static const setPassword = 'set-password';