| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- 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 '../../../core/storage/local_storage.dart';
- import '../../MinePage/data/change_password_provider.dart';
- import '../../MinePage/presentation/change_password_page.dart';
- import '../domain/login_service.dart';
- import '../data/login_provider.dart';
- class LoginPage extends ConsumerStatefulWidget {
- const LoginPage({super.key});
- @override
- ConsumerState<LoginPage> createState() => _LoginPageState();
- }
- class _LoginPageState extends ConsumerState<LoginPage>
- with SingleTickerProviderStateMixin {
- late TabController _tabController;
- final _passwordFormKey = GlobalKey<FormState>();
- final _smsFormKey = GlobalKey<FormState>();
- 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);
- _loadSavedLoginInfo();
- }
- /// 加载保存的登录信息
- void _loadSavedLoginInfo() async {
- try {
- // 加载保存的手机号
- final savedPhone = await LocalStorage.getPhone();
- if (savedPhone != null && savedPhone.isNotEmpty) {
- _phoneController.text = savedPhone;
- _phoneSmsController.text = savedPhone;
- // 同步到 Provider,避免监听器尚未注册导致 Provider 仍为空
- if (mounted) {
- ref.read(passwordLoginPhoneProvider.notifier).state = savedPhone;
- ref.read(smsLoginPhoneProvider.notifier).state = savedPhone;
- }
- AppLogger.d('加载保存的手机号: $savedPhone');
- }
- // 检查是否记住密码
- final rememberPassword = await LocalStorage.getRememberPassword();
- if (mounted) {
- ref.read(rememberPasswordProvider.notifier).state = rememberPassword;
- // 回到登录页时,强制隐藏密码(无论前一次是否显示)
- ref.read(passwordObscureProvider.notifier).state = true;
- }
- if (rememberPassword) {
- // 加载保存的密码(原始密码)
- final savedPassword = await LocalStorage.getPassword();
- if (savedPassword != null && savedPassword.isNotEmpty) {
- // 设置密码(但保持隐藏状态)
- _passwordController.text = savedPassword;
- // 同步到 Provider,避免监听器尚未注册导致 Provider 仍为空
- if (mounted) {
- ref.read(passwordLoginPasswordProvider.notifier).state = savedPassword;
- }
- AppLogger.d('加载保存的密码(原始密码)');
- }
- }
- } catch (e) {
- AppLogger.e('加载保存的登录信息失败', e);
- }
- }
- void _setupListeners() {
- // 先强制同步一次当前 Controller 的值到 Provider(避免 _loadSavedLoginInfo 在 listener 注册前完成)
- ref.read(passwordLoginPhoneProvider.notifier).state = _phoneController.text;
- ref.read(passwordLoginPasswordProvider.notifier).state = _passwordController.text;
- ref.read(smsLoginPhoneProvider.notifier).state = _phoneSmsController.text;
- ref.read(smsLoginCodeProvider.notifier).state = _smsCodeController.text;
-
- // 监听 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.phoneNumberRequired);
- 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);
- final rememberPassword = ref.read(rememberPasswordProvider);
- // 每次登录请求时重新加密(不存 MD5)
- final encryptedPassword = CryptoUtils.md5(password);
- AppLogger.d('开始密码登录请求: $phoneNumber');
- // 调用登录服务
- await LoginService.passwordLogin(
- mobile: phoneNumber,
- password: encryptedPassword,
- loginSystem: _loginSystem,
- loginType: _loginType,
- );
- AppLogger.d('密码登录成功,准备跳转主页');
-
- // 登录成功后,保存手机号(总是保存)
- await LocalStorage.savePhone(phoneNumber);
-
- // 根据"记住密码"选项保存或删除密码
- if (rememberPassword) {
- // 保存原始密码(登录时再加密)
- await LocalStorage.savePassword(password);
- await LocalStorage.saveRememberPassword(true);
- AppLogger.d('已保存手机号和密码');
- } else {
- // 不记住密码时,删除已保存的密码
- await LocalStorage.removePassword();
- await LocalStorage.saveRememberPassword(false);
- AppLogger.d('已保存手机号,删除密码');
- }
-
- // 跳转到主页(使用 go 替换当前路由,销毁登录页)
- 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 = '';
- // 跳转到主页(使用 go 替换当前路由,销毁登录页)
- 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.phoneNumberRequired,
- 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.passwordRequired,
- prefixIcon: const Icon(Icons.lock_outline),
- suffixIcon: IconButton(
- icon: Icon(
- obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_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),
- // 记住密码 + 忘记密码
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- // 记住密码复选框
- Consumer(
- builder: (context, ref, child) {
- final rememberPassword = ref.watch(rememberPasswordProvider);
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Checkbox(
- value: rememberPassword,
- onChanged: (value) {
- ref.read(rememberPasswordProvider.notifier).state = value ?? false;
- },
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
- visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
- ),
- const SizedBox(width: 4),
- GestureDetector(
- onTap: () {
- ref.read(rememberPasswordProvider.notifier).state = !rememberPassword;
- },
- child: Text(
- l10n.rememberPassword,
- style: const TextStyle(
- fontSize: 14,
- color: Color(0xFF6B7280),
- ),
- ),
- ),
- ],
- );
- },
- ),
- // 忘记密码
- TextButton(
- onPressed: () {
- // 跳转到忘记密码页面
- if (mounted) {
- // 打开对话框前重置所有状态,确保每次打开都是干净的
- ref.read(changePwdPhoneProvider.notifier).state = '';
- ref.read(changePwdServerSmsCodeProvider.notifier).state = '';
- ref.read(changePwdPasswordProvider.notifier).state = '';
- ref.read(changePwdPasswordVisibleProvider.notifier).state = false;
- ref.read(changePwdConfirmPasswordProvider.notifier).state = '';
- ref.read(changePwdConfirmPasswordVisibleProvider.notifier).state = false;
- ref.read(changePwdSmsHasSentProvider.notifier).state = false;
- ref.read(changePwdSmsCountdownProvider.notifier).state = 0;
- ref.read(changePwdShowPasswordErrorProvider.notifier).state = false;
-
- showDialog(
- context: context,
- builder: (context) => const ChangePasswordPage(),
- );
- }
- },
- 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.phoneNumberRequired,
- 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.smsCodeRequired,
- 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),
- ),
- ],
- ),
- );
- }
- }
|