import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sino_med_cloud/l10n/app_localizations.dart'; import 'package:sino_med_cloud/core/constants/app_constants.dart'; import 'package:sino_med_cloud/core/utils/toast_utils.dart'; // 添加 ToastUtils 引用 import '../../../core/utils/logger.dart'; import '../../../core/utils/crypto_utils.dart'; import '../domain/login_service.dart'; import '../data/login_provider.dart'; class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final _passwordFormKey = GlobalKey(); final _smsFormKey = GlobalKey(); bool _listenersSetup = false; //用于test final _loginSystem = "YUN_HIS_PC_WEB"; final _loginType = "MOBILE_PASSWORD"; final _loginSmsType = "MOBILE_SMS_CODE"; // 密码登录表单 final _phoneController = TextEditingController(); final _passwordController = TextEditingController(); // 验证码登录表单 final _phoneSmsController = TextEditingController(); final _smsCodeController = TextEditingController(); Timer? _countdownTimer; // 防止循环更新的标志 bool _isSyncingPhone = false; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } void _setupListeners() { // 监听 Tab 切换,同步到 Provider _tabController.addListener(() { if (!_tabController.indexIsChanging) { ref.read(loginTabIndexProvider.notifier).state = _tabController.index; } }); // 同步初始 tab 索引 ref.read(loginTabIndexProvider.notifier).state = _tabController.index; // 监听输入框变化,同步到 Provider 和另一个输入框 _phoneController.addListener(() { final text = _phoneController.text; ref.read(passwordLoginPhoneProvider.notifier).state = text; // 同步到验证码登录的手机号输入框 if (!_isSyncingPhone && _phoneSmsController.text != text) { _isSyncingPhone = true; _phoneSmsController.text = text; ref.read(smsLoginPhoneProvider.notifier).state = text; _isSyncingPhone = false; } }); _passwordController.addListener(() { ref.read(passwordLoginPasswordProvider.notifier).state = _passwordController.text; }); _phoneSmsController.addListener(() { final text = _phoneSmsController.text; ref.read(smsLoginPhoneProvider.notifier).state = text; // 同步到密码登录的手机号输入框 if (!_isSyncingPhone && _phoneController.text != text) { _isSyncingPhone = true; _phoneController.text = text; ref.read(passwordLoginPhoneProvider.notifier).state = text; _isSyncingPhone = false; } }); _smsCodeController.addListener(() { ref.read(smsLoginCodeProvider.notifier).state = _smsCodeController.text; }); } @override void dispose() { _tabController.dispose(); _phoneController.dispose(); _passwordController.dispose(); _phoneSmsController.dispose(); _smsCodeController.dispose(); _clearCountdownTimer(); super.dispose(); } /// 清理倒计时定时器 void _clearCountdownTimer() { _countdownTimer?.cancel(); _countdownTimer = null; } // 发送验证码 void _sendSmsCode() async { final l10n = AppLocalizations.of(context)!; final phone = ref.read(smsLoginPhoneProvider); if (phone.isEmpty) { ToastUtils.show(l10n.phoneNumberRequiredForSms); return; } try { // 清理之前的定时器 _clearCountdownTimer(); // 调用发送验证码服务 final smsCode = await LoginService.sendSmsCode(mobile: phone); // 保存验证码到 Provider ref.read(smsCodeFromServerProvider.notifier).state = smsCode; // 标记已获取过验证码 ref.read(smsHasReceivedProvider.notifier).state = true; // 开始倒计时 ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown; // 使用 Timer 进行倒计时 _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) { timer.cancel(); return; } final currentCountdown = ref.read(smsCountdownProvider); if (currentCountdown > 1) { ref.read(smsCountdownProvider.notifier).state = currentCountdown - 1; } else { // 倒计时结束(currentCountdown == 1 或 0) // 先清空验证码,再设置倒计时为0,确保 Consumer 能正确监听到变化 ref.read(smsCodeFromServerProvider.notifier).state = ''; ref.read(smsCountdownProvider.notifier).state = 0; timer.cancel(); _countdownTimer = null; AppLogger.d('倒计时结束,验证码已清空,hasReceived: ${ref.read(smsHasReceivedProvider)}'); } }); } catch (e) { AppLogger.e('发送验证码错误', e); if (mounted) { ToastUtils.showError(e.toString()); } } } // 密码登录 void _handlePasswordLogin() async { try { // 当前手机号及密码的格式已经验证过 if (_passwordFormKey.currentState!.validate()) { // 在异步方法中应使用 ref.read final phoneNumber = ref.read(passwordLoginPhoneProvider); final password = ref.read(passwordLoginPasswordProvider); // 对密码进行 MD5 加密 final encryptedPassword = CryptoUtils.md5(password); AppLogger.d('开始密码登录请求: $phoneNumber'); // 调用登录服务 await LoginService.passwordLogin( mobile: phoneNumber, password: encryptedPassword, loginSystem: _loginSystem, loginType: _loginType, ); AppLogger.d('密码登录成功,准备跳转主页'); // 登录成功,跳转到主页 if (mounted) { context.go('/mainTab'); } } } catch (e, stack) { AppLogger.e('密码登录错误', e, stack); if (mounted) { ToastUtils.showError(e.toString().replaceAll('Exception: ', '')); } } } // 验证码登录 void _handleSmsLogin() async { try { final l10n = AppLocalizations.of(context)!; if (_smsFormKey.currentState!.validate()) { // 在异步方法中应使用 ref.read final smsCode = ref.read(smsLoginCodeProvider); final serverSmsCode = ref.read(smsCodeFromServerProvider); // 检查验证码是否已过期(为空或倒计时已结束) if (serverSmsCode.isEmpty) { ToastUtils.show(l10n.smsCodeHasExpired); return; } // 验证码输入错误 if (smsCode != serverSmsCode) { ToastUtils.showError(l10n.smsCodeError); return; } final phoneNumber = ref.read(smsLoginPhoneProvider); AppLogger.d('开始验证码登录请求: $phoneNumber'); // 调用登录服务 await LoginService.smsLogin( mobile: phoneNumber, smsCode: smsCode, loginSystem: _loginSystem, loginType: _loginSmsType, ); AppLogger.d('验证码登录成功,清理状态并跳转主页'); // 登录成功,清理倒计时和定时器 _clearCountdownTimer(); ref.read(smsCountdownProvider.notifier).state = 0; ref.read(smsHasReceivedProvider.notifier).state = false; ref.read(smsCodeFromServerProvider.notifier).state = ''; // 跳转到主页 if (mounted) { context.go('/mainTab'); } } } catch (e, stack) { AppLogger.e('验证码登录错误', e, stack); if (mounted) { ToastUtils.showError(e.toString().replaceAll('Exception: ', '')); } } } @override Widget build(BuildContext context) { // 设置监听器(只设置一次) if (!_listenersSetup) { WidgetsBinding.instance.addPostFrameCallback((_) { _setupListeners(); _listenersSetup = true; }); } final l10n = AppLocalizations.of(context)!; return Scaffold( body: GestureDetector( onTap: () { // 点击空白区域时收起键盘并移除焦点 FocusScope.of(context).unfocus(); }, behavior: HitTestBehavior.opaque, child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 40), // Logo 或标题 Text( l10n.appName, style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF1F2937), ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( l10n.appSubtitle, style: const TextStyle( fontSize: 16, color: Color(0xFF6B7280), ), textAlign: TextAlign.center, ), const SizedBox(height: 48), // Tab 切换 Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: TabBar( controller: _tabController, indicator: BoxDecoration( borderRadius: BorderRadius.circular(12), color: const Color(0xFF00BFA5), ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, labelColor: Colors.white, unselectedLabelColor: const Color(0xFF6B7280), labelStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), unselectedLabelStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), tabs: [ Tab(text: l10n.passwordLogin), Tab(text: l10n.smsLogin), ], ), ), const SizedBox(height: 24), // Tab 内容 SizedBox( height: 400, child: TabBarView( controller: _tabController, children: [ _buildPasswordLoginForm(), _buildSmsLoginForm(), ], ), ), ], ), ), ), ), ); } // 密码登录表单 Widget _buildPasswordLoginForm() { final l10n = AppLocalizations.of(context)!; return Form( key: _passwordFormKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 4), // 手机号输入 TextFormField( controller: _phoneController, keyboardType: TextInputType.phone, decoration: InputDecoration( labelText: l10n.phoneNumber, hintText: l10n.phoneNumberHint, prefixIcon: const Icon(Icons.phone_outlined), ), validator: (value) { if (value == null || value.isEmpty) { return l10n.phoneNumberRequired; } if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) { return l10n.phoneNumberInvalid; } return null; }, ), const SizedBox(height: 16), // 密码输入 Consumer( builder: (context, ref, child) { final obscurePassword = ref.watch(passwordObscureProvider); return TextFormField( controller: _passwordController, obscureText: obscurePassword, decoration: InputDecoration( labelText: l10n.password, hintText: l10n.passwordHint, prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon( obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () { ref.read(passwordObscureProvider.notifier).state = !obscurePassword; }, ), ), validator: (value) { if (value == null || value.isEmpty) { return l10n.passwordRequired; } if (value.length < AppConstants.passwordMinLength) { return l10n.passwordMinLength; } return null; }, ); }, ), const SizedBox(height: 8), // 忘记密码 Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { // 跳转到忘记密码页面 if (mounted) { context.push('/ChangePassword'); } }, child: Text(l10n.forgotPassword), ), ), const SizedBox(height: 24), // 登录按钮 ElevatedButton( onPressed: _handlePasswordLogin, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), child: Text(l10n.login), ), ], ), ); } // 验证码登录表单 Widget _buildSmsLoginForm() { final l10n = AppLocalizations.of(context)!; return Form( key: _smsFormKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 4), // 手机号输入 TextFormField( controller: _phoneSmsController, keyboardType: TextInputType.phone, decoration: InputDecoration( labelText: l10n.phoneNumber, hintText: l10n.phoneNumberHint, prefixIcon: const Icon(Icons.phone_outlined), ), validator: (value) { if (value == null || value.isEmpty) { return l10n.phoneNumberRequired; } if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) { return l10n.phoneNumberInvalid; } return null; }, ), const SizedBox(height: 16), // 验证码输入 Row( children: [ Expanded( child: TextFormField( controller: _smsCodeController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: l10n.smsCode, hintText: l10n.smsCodeHint, prefixIcon: const Icon(Icons.sms_outlined), ), validator: (value) { // 验证码为空 if (value == null || value.isEmpty) { return l10n.smsCodeRequired; } // 验证码格式不正确(长度或格式) if (value.length != AppConstants.smsCodeLength || !RegExp(r'^\d+$').hasMatch(value)) { return l10n.smsCodeInvalid; } return null; }, ), ), const SizedBox(width: 12), Consumer( builder: (context, ref, child) { final countdown = ref.watch(smsCountdownProvider); final hasReceived = ref.watch(smsHasReceivedProvider); final serverSmsCode = ref.watch(smsCodeFromServerProvider); // 验证码已过期:曾经获取过验证码,但现在验证码为空且倒计时为0 final isExpired = hasReceived && serverSmsCode.isEmpty && countdown == 0; // 调试日志 if (countdown == 0 && hasReceived) { AppLogger.d('按钮状态 - countdown: $countdown, hasReceived: $hasReceived, serverSmsCode: $serverSmsCode, isExpired: $isExpired'); } return SizedBox( width: 100, child: ElevatedButton( onPressed: countdown > 0 ? null : _sendSmsCode, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: countdown > 0 ? const Color(0xFFE5E7EB) : const Color(0xFF00BFA5), ), child: Text( countdown > 0 ? l10n.smsCodeCountdown(countdown) : (isExpired ? l10n.resendSmsCode : l10n.getSmsCode), style: TextStyle( color: countdown > 0 ? const Color(0xFF6B7280) : Colors.white, fontSize: isExpired ? 12 : 14, ), ), ), ); }, ), ], ), const SizedBox(height: 24), // 登录按钮 ElevatedButton( onPressed: _handleSmsLogin, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), child: Text(l10n.login), ), ], ), ); } }