import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:path/path.dart' as path; import 'package:sino_med_cloud/l10n/app_localizations.dart'; import 'package:sino_med_cloud/core/constants/app_constants.dart'; import '../../../core/constants/api_constants.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/storage/local_storage.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/crypto_utils.dart'; import 'login_provider.dart'; import 'package:dio/dio.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 _phoneController = TextEditingController(); final _passwordController = TextEditingController(); // 验证码登录表单 final _phoneSmsController = TextEditingController(); final _smsCodeController = TextEditingController(); @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(() { ref.read(passwordLoginPhoneProvider.notifier).state = _phoneController.text; }); _passwordController.addListener(() { ref.read(passwordLoginPasswordProvider.notifier).state = _passwordController.text; }); _phoneSmsController.addListener(() { ref.read(smsLoginPhoneProvider.notifier).state = _phoneSmsController.text; }); _smsCodeController.addListener(() { ref.read(smsLoginCodeProvider.notifier).state = _smsCodeController.text; }); } @override void dispose() { _tabController.dispose(); _phoneController.dispose(); _passwordController.dispose(); _phoneSmsController.dispose(); _smsCodeController.dispose(); super.dispose(); } // 发送验证码 void _sendSmsCode() { final l10n = AppLocalizations.of(context)!; final phone = ref.read(smsLoginPhoneProvider); if (phone.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.phoneNumberRequiredForSms)), ); return; } // TODO: 调用发送验证码接口 // 开始倒计时 ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown; // 倒计时 Future.doWhile(() async { await Future.delayed(const Duration(seconds: 1)); if (mounted) { final currentCountdown = ref.read(smsCountdownProvider); if (currentCountdown > 0) { ref.read(smsCountdownProvider.notifier).state = currentCountdown - 1; } } return ref.read(smsCountdownProvider) > 0; }); } // 密码登录 void _handlePasswordLogin() async { try { // 当前手机号及密码的格式已经验证过 if (_passwordFormKey.currentState!.validate()) { final phoneNumber = ref.watch(passwordLoginPhoneProvider); final password = ref.watch(passwordLoginPasswordProvider); // 对密码进行 MD5 加密 final encryptedPassword = CryptoUtils.md5(password); final parame = { "mobile": phoneNumber, "login_system": _loginSystem, "password": encryptedPassword, "login_type": _loginType }; AppLogger.d('登录请求参数parame: $parame'); Response response = await DioClient.post>( path.join(ApiConstants.baseUrl, ApiConstants.login), data: parame, ); if (response.statusCode == 200) { final data = response.data; if (data['code'] == 20000) { AppLogger.d('登录成功: $data'); final jsonData = data['data']; final accessToken = jsonData['access_token']; final userInfo = jsonData['user_info']; LocalStorage.saveToken(accessToken); LocalStorage.saveUserInfo(userInfo); if (mounted) { context.replace('/mainTab'); } } else { AppLogger.d('登录失败: $data'); } } else { AppLogger.d('密码登录请求错误: ${response.statusCode}, ${response.statusMessage}'); } } } catch (e) { AppLogger.e('密码登录错误:handlePasswordLogin', e); rethrow; } } // 验证码登录 void _handleSmsLogin() { final l10n = AppLocalizations.of(context)!; if (_smsFormKey.currentState!.validate()) { // TODO: 调用登录接口 // ScaffoldMessenger.of(context).showSnackBar( // SnackBar(content: Text(l10n.loginNotImplemented)), // ); context.push('/mainTab'); } } @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: () { // TODO: 跳转到忘记密码页面 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.forgotPasswordNotImplemented)), ); }, 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) { return l10n.smsCodeLength; } return null; }, ), ), const SizedBox(width: 12), Consumer( builder: (context, ref, child) { final countdown = ref.watch(smsCountdownProvider); 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) : l10n.getSmsCode, style: TextStyle( color: countdown > 0 ? const Color(0xFF6B7280) : Colors.white, ), ), ), ); }, ), ], ), const SizedBox(height: 24), // 登录按钮 ElevatedButton( onPressed: _handleSmsLogin, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), child: Text(l10n.login), ), ], ), ); } }