فهرست منبع

登录接口调用。

PC\19500 4 هفته پیش
والد
کامیت
ef4c57f83b

+ 7 - 15
lib/core/constants/api_constants.dart

@@ -5,33 +5,25 @@ class ApiConstants {
   // ==================== 基础配置 ====================
   
   /// API 基础地址(需要根据实际环境配置)
-  static const String baseUrl = 'https://api.example.com';
-  
-  /// API 版本
-  static const String apiVersion = '/api/v1';
-  
-  /// 完整的基础 URL
-  static String get baseApiUrl => '$baseUrl$apiVersion';
+  /// qa
+  static const String baseUrl = 'https://qaapi.lightcura.com';
 
   // ==================== 认证相关 ====================
   
   /// 登录接口
-  static const String login = '/auth/login';
-  
-  /// 验证码登录接口
-  static const String loginWithSms = '/auth/login/sms';
-  
+  static const String login = 'api/his-auth/user/login/';
+
   /// 发送验证码接口
-  static const String sendSmsCode = '/auth/sms/send';
+  static const String sendSmsCode = 'api/his-auth/send/sms-code/';
   
   /// 刷新 Token 接口
   static const String refreshToken = '/auth/refresh';
   
   /// 登出接口
-  static const String logout = '/auth/logout';
+  static const String logout = 'api/his-auth/user/logout/';
   
   /// 忘记密码接口
-  static const String forgotPassword = '/auth/password/forgot';
+  static const String forgotPassword = 'api/his-user/institution_users/change-password/';
   
   /// 重置密码接口
   static const String resetPassword = '/auth/password/reset';

+ 1 - 1
lib/core/constants/app_constants.dart

@@ -16,7 +16,7 @@ class AppConstants {
   // ==================== 存储 Key ====================
   
   /// Token 存储 Key
-  static const String keyToken = 'auth_token';
+  static const String keyToken = 'access_token';
   
   /// 用户信息存储 Key
   static const String keyUserInfo = 'user_info';

+ 172 - 2
lib/core/network/dio_client.dart

@@ -3,13 +3,12 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
 import 'package:sino_med_cloud/core/constants/api_constants.dart';
 import 'package:sino_med_cloud/core/network/interceptors/auth_interceptor.dart';
 import 'package:sino_med_cloud/core/network/interceptors/error_interceptor.dart';
-import 'package:sino_med_cloud/core/network/interceptors/log_interceptor.dart';
 
 /// Dio 客户端 - 统一网络请求管理
 class DioClient {
   static final Dio _dio = Dio(
     BaseOptions(
-      baseUrl: ApiConstants.baseApiUrl,
+      baseUrl: ApiConstants.baseUrl,
       connectTimeout: Duration(seconds: ApiConstants.connectTimeout),
       receiveTimeout: Duration(seconds: ApiConstants.receiveTimeout),
       sendTimeout: Duration(seconds: ApiConstants.sendTimeout),
@@ -44,4 +43,175 @@ class DioClient {
   static void reset() {
     _dio.interceptors.clear();
   }
+
+  // ==================== GET 请求 ====================
+
+  /// GET 请求
+  /// 
+  /// [path] 请求路径(相对于 baseUrl)
+  /// [queryParameters] 查询参数
+  /// [options] 请求选项
+  /// 
+  /// 返回 [Response] 对象
+  static Future<Response<T>> get<T>(
+    String path, {
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+    ProgressCallback? onReceiveProgress,
+  }) async {
+    try {
+      final response = await dio.get<T>(
+        path,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+        onReceiveProgress: onReceiveProgress,
+      );
+      return response;
+    } on DioException catch (e) {
+      // 错误已由 ErrorInterceptor 处理,直接抛出
+      rethrow;
+    }
+  }
+
+  // ==================== POST 请求 ====================
+
+  /// POST 请求
+  /// 
+  /// [path] 请求路径(相对于 baseUrl)
+  /// [data] 请求体数据
+  /// [queryParameters] 查询参数
+  /// [options] 请求选项
+  /// 
+  /// 返回 [Response] 对象
+  static Future<Response<T>> post<T>(
+    String path, {
+    dynamic data,
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+    ProgressCallback? onSendProgress,
+    ProgressCallback? onReceiveProgress,
+  }) async {
+    try {
+      final response = await dio.post<T>(
+        path,
+        data: data,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+        onSendProgress: onSendProgress,
+        onReceiveProgress: onReceiveProgress,
+      );
+      return response;
+    } on DioException catch (e) {
+      // 错误已由 ErrorInterceptor 处理,直接抛出
+      rethrow;
+    }
+  }
+
+  // ==================== DELETE 请求 ====================
+
+  /// DELETE 请求
+  /// 
+  /// [path] 请求路径(相对于 baseUrl)
+  /// [data] 请求体数据(可选)
+  /// [queryParameters] 查询参数
+  /// [options] 请求选项
+  /// 
+  /// 返回 [Response] 对象
+  static Future<Response<T>> delete<T>(
+    String path, {
+    dynamic data,
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+  }) async {
+    try {
+      final response = await dio.delete<T>(
+        path,
+        data: data,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+      );
+      return response;
+    } on DioException catch (e) {
+      // 错误已由 ErrorInterceptor 处理,直接抛出
+      rethrow;
+    }
+  }
+
+  // ==================== PUT 请求====================
+
+  /// PUT 请求
+  /// 
+  /// [path] 请求路径(相对于 baseUrl)
+  /// [data] 请求体数据
+  /// [queryParameters] 查询参数
+  /// [options] 请求选项
+  /// 
+  /// 返回 [Response] 对象
+  static Future<Response<T>> put<T>(
+    String path, {
+    dynamic data,
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+    ProgressCallback? onSendProgress,
+    ProgressCallback? onReceiveProgress,
+  }) async {
+    try {
+      final response = await dio.put<T>(
+        path,
+        data: data,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+        onSendProgress: onSendProgress,
+        onReceiveProgress: onReceiveProgress,
+      );
+      return response;
+    } on DioException catch (e) {
+      // 错误已由 ErrorInterceptor 处理,直接抛出
+      rethrow;
+    }
+  }
+
+  // ==================== PATCH 请求 ====================
+
+  /// PATCH 请求
+  /// 
+  /// [path] 请求路径(相对于 baseUrl)
+  /// [data] 请求体数据
+  /// [queryParameters] 查询参数
+  /// [options] 请求选项
+  /// 
+  /// 返回 [Response] 对象
+  static Future<Response<T>> patch<T>(
+    String path, {
+    dynamic data,
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+    ProgressCallback? onSendProgress,
+    ProgressCallback? onReceiveProgress,
+  }) async {
+    try {
+      final response = await dio.patch<T>(
+        path,
+        data: data,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+        onSendProgress: onSendProgress,
+        onReceiveProgress: onReceiveProgress,
+      );
+      return response;
+    } on DioException catch (e) {
+      // 错误已由 ErrorInterceptor 处理,直接抛出
+      rethrow;
+    }
+  }
 }

+ 21 - 20
lib/core/network/interceptors/log_interceptor.dart

@@ -1,4 +1,5 @@
 import 'package:dio/dio.dart';
+import 'package:sino_med_cloud/core/utils/logger.dart';
 
 /// 日志拦截器 - 记录请求和响应日志
 /// 注意:项目中已使用 pretty_dio_logger,此拦截器用于自定义日志记录
@@ -19,47 +20,47 @@ class LogInterceptor extends Interceptor {
 
   @override
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
-    print('┌─────────────────────────────────────────────────────────────');
-    print('│ Request: ${options.method} ${options.uri}');
+    AppLogger.d('┌─────────────────────────────────────────────────────────────');
+    AppLogger.d('│ Request: ${options.method} ${options.uri}');
     
     if (requestHeader && options.headers.isNotEmpty) {
-      print('│ Headers:');
+      AppLogger.d('│ Headers:');
       options.headers.forEach((key, value) {
         // 隐藏敏感信息
         if (key.toLowerCase() == 'authorization') {
-          print('│   $key: ${value.toString().substring(0, 20)}...');
+          AppLogger.d('│   $key: ${value.toString().substring(0, 20)}...');
         } else {
-          print('│   $key: $value');
+          AppLogger.d('│   $key: $value');
         }
       });
     }
     
     if (requestBody && options.data != null) {
-      print('│ Body: ${options.data}');
+      AppLogger.d('│ Body: ${options.data}');
     }
     
-    print('└─────────────────────────────────────────────────────────────');
+    AppLogger.d('└─────────────────────────────────────────────────────────────');
     
     super.onRequest(options, handler);
   }
 
   @override
   void onResponse(Response response, ResponseInterceptorHandler handler) {
-    print('┌─────────────────────────────────────────────────────────────');
-    print('│ Response: ${response.statusCode} ${response.requestOptions.uri}');
+    AppLogger.d('┌─────────────────────────────────────────────────────────────');
+    AppLogger.d('│ Response: ${response.statusCode} ${response.requestOptions.uri}');
     
     if (responseHeader && response.headers.map.isNotEmpty) {
-      print('│ Headers:');
+      AppLogger.d('│ Headers:');
       response.headers.map.forEach((key, value) {
-        print('│   $key: $value');
+        AppLogger.d('│   $key: $value');
       });
     }
     
     if (responseBody && response.data != null) {
-      print('│ Body: ${response.data}');
+      AppLogger.d('│ Body: ${response.data}');
     }
     
-    print('└─────────────────────────────────────────────────────────────');
+    AppLogger.d('└─────────────────────────────────────────────────────────────');
     
     super.onResponse(response, handler);
   }
@@ -67,15 +68,15 @@ class LogInterceptor extends Interceptor {
   @override
   void onError(DioException err, ErrorInterceptorHandler handler) {
     if (error) {
-      print('┌─────────────────────────────────────────────────────────────');
-      print('│ Error: ${err.type}');
-      print('│ Request: ${err.requestOptions.method} ${err.requestOptions.uri}');
-      print('│ Message: ${err.error ?? err.message}');
+      AppLogger.e('┌─────────────────────────────────────────────────────────────');
+      AppLogger.e('│ Error: ${err.type}');
+      AppLogger.e('│ Request: ${err.requestOptions.method} ${err.requestOptions.uri}');
+      AppLogger.e('│ Message: ${err.error ?? err.message}');
       if (err.response != null) {
-        print('│ Status Code: ${err.response?.statusCode}');
-        print('│ Response: ${err.response?.data}');
+        AppLogger.e('│ Status Code: ${err.response?.statusCode}');
+        AppLogger.e('│ Response: ${err.response?.data}');
       }
-      print('└─────────────────────────────────────────────────────────────');
+      AppLogger.e('└─────────────────────────────────────────────────────────────');
     }
     
     super.onError(err, handler);

+ 7 - 6
lib/core/storage/local_storage.dart

@@ -1,4 +1,5 @@
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:sino_med_cloud/core/constants/app_constants.dart';
 
 /// 本地存储管理类
 class LocalStorage {
@@ -103,32 +104,32 @@ class LocalStorage {
 
   /// 保存 Token
   static Future<bool> saveToken(String token) async {
-    return await setString('auth_token', token);
+    return await setString(AppConstants.keyToken, token);
   }
 
   /// 获取 Token
   static String? getToken() {
-    return getString('auth_token');
+    return getString(AppConstants.keyToken);
   }
 
   /// 删除 Token
   static Future<bool> removeToken() async {
-    return await remove('auth_token');
+    return await remove(AppConstants.keyToken);
   }
 
   /// 保存用户信息(JSON 字符串)
   static Future<bool> saveUserInfo(String userInfo) async {
-    return await setString('user_info', userInfo);
+    return await setString(AppConstants.keyUserInfo, userInfo);
   }
 
   /// 获取用户信息
   static String? getUserInfo() {
-    return getString('user_info');
+    return getString(AppConstants.keyUserInfo);
   }
 
   /// 删除用户信息
   static Future<bool> removeUserInfo() async {
-    return await remove('user_info');
+    return await remove(AppConstants.keyUserInfo);
   }
 }
 

+ 51 - 0
lib/core/utils/crypto_utils.dart

@@ -0,0 +1,51 @@
+import 'dart:convert';
+import 'package:crypto/crypto.dart' as crypto;
+
+/// 加密工具类
+/// 对应 Web 端 ts-md5 库的 Md5.hashStr() 方法
+class CryptoUtils {
+  CryptoUtils._();
+
+  /// MD5 加密
+  /// 
+  /// 对应 Web 端 ts-md5 库的 `Md5.hashStr(input)` 方法
+  /// 使用 UTF-8 编码,返回小写十六进制字符串
+  /// 
+  /// [input] 要加密的字符串
+  /// 返回 MD5 加密后的十六进制字符串(小写,32 位)
+  /// 
+  /// 示例:
+  /// ```dart
+  /// CryptoUtils.md5('password') // 返回 '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'
+  /// ```
+  static String md5(String input) {
+    // 使用 UTF-8 编码,与 ts-md5 的 hashStr 方法一致
+    // ts-md5 的 hashStr 方法使用 UTF-8 编码处理字符串
+    final bytes = utf8.encode(input);
+    final digest = crypto.md5.convert(bytes);
+    // 返回小写十六进制字符串,与 ts-md5 的 hashStr 返回格式一致
+    // ts-md5 的 hashStr 返回的是小写十六进制字符串
+    return digest.toString();
+  }
+
+  /// SHA256 加密
+  /// 
+  /// [input] 要加密的字符串
+  /// 返回 SHA256 加密后的十六进制字符串(小写)
+  static String sha256(String input) {
+    final bytes = utf8.encode(input);
+    final digest = crypto.sha256.convert(bytes);
+    return digest.toString();
+  }
+
+  /// SHA1 加密
+  /// 
+  /// [input] 要加密的字符串
+  /// 返回 SHA1 加密后的十六进制字符串(小写)
+  static String sha1(String input) {
+    final bytes = utf8.encode(input);
+    final digest = crypto.sha1.convert(bytes);
+    return digest.toString();
+  }
+}
+

+ 24 - 2
lib/features/MinePage/presentation/mine_page.dart

@@ -1,5 +1,9 @@
 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 '../../main_tab_provider.dart';
 
 class MinePage extends ConsumerStatefulWidget {
   const MinePage({super.key});
@@ -12,9 +16,27 @@ class _MinePageState extends ConsumerState<MinePage>
     with SingleTickerProviderStateMixin {
   @override
   Widget build(BuildContext context) {
-    return const Center(
-      child: Text('我的'),
+    final l10n = AppLocalizations.of(context)!;
+    return Center(
+      child: // 退出登录按钮
+      ElevatedButton(
+        onPressed: _handleLogout,
+        style: ElevatedButton.styleFrom(
+          padding: EdgeInsets.symmetric(vertical: 16),
+        ),
+        child: Text(l10n.logout),
+      ),
     );
   }
 
+  // 退出登录
+  void _handleLogout() {
+    ref.read(currentTabIndexProvider.notifier).state = 0;
+    Future.microtask(() {
+      if (mounted) {
+        context.replace('/');
+      }
+    });
+  }
+
 }

+ 140 - 63
lib/features/auth/presentation/login_page.dart

@@ -2,6 +2,14 @@ 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 '../../../core/constants/api_constants.dart';
+import '../../../core/network/dio_client.dart';
+import '../../../core/utils/logger.dart';
+import '../../../core/utils/crypto_utils.dart';
+import 'login_provider.dart';
+import 'package:dio/dio.dart';
+import 'package:path/path.dart' as path;
 
 class LoginPage extends ConsumerStatefulWidget {
   const LoginPage({super.key});
@@ -15,16 +23,19 @@ class _LoginPageState extends ConsumerState<LoginPage>
   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 _phoneController = TextEditingController();
   final _passwordController = TextEditingController();
-  bool _obscurePassword = true;
 
   // 验证码登录表单
   final _phoneSmsController = TextEditingController();
   final _smsCodeController = TextEditingController();
-  int _countdown = 0;
 
   @override
   void initState() {
@@ -32,6 +43,30 @@ class _LoginPageState extends ConsumerState<LoginPage>
     _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();
@@ -45,7 +80,8 @@ class _LoginPageState extends ConsumerState<LoginPage>
   // 发送验证码
   void _sendSmsCode() {
     final l10n = AppLocalizations.of(context)!;
-    if (_phoneSmsController.text.isEmpty) {
+    final phone = ref.read(smsLoginPhoneProvider);
+    if (phone.isEmpty) {
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(content: Text(l10n.phoneNumberRequiredForSms)),
       );
@@ -53,31 +89,56 @@ class _LoginPageState extends ConsumerState<LoginPage>
     }
 
     // TODO: 调用发送验证码接口
-    setState(() {
-      _countdown = 60;
-    });
+    // 开始倒计时
+    ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown;
 
     // 倒计时
     Future.doWhile(() async {
       await Future.delayed(const Duration(seconds: 1));
       if (mounted) {
-        setState(() {
-          _countdown--;
-        });
+        final currentCountdown = ref.read(smsCountdownProvider);
+        if (currentCountdown > 0) {
+          ref.read(smsCountdownProvider.notifier).state = currentCountdown - 1;
+        }
       }
-      return _countdown > 0;
+      return ref.read(smsCountdownProvider) > 0;
     });
   }
 
   // 密码登录
-  void _handlePasswordLogin() {
-    final l10n = AppLocalizations.of(context)!;
+  void _handlePasswordLogin() async {
+    // 当前手机号及密码的格式已经验证过
     if (_passwordFormKey.currentState!.validate()) {
-      // TODO: 调用登录接口
-      // ScaffoldMessenger.of(context).showSnackBar(
-      //   SnackBar(content: Text(l10n.loginNotImplemented)),
-      // );
-      context.go('/mainTab');
+      final phoneNumber = ref.watch(passwordLoginPhoneProvider);
+      final password = ref.watch(passwordLoginPasswordProvider);
+
+      // 对密码进行 MD5 加密
+      final encryptedPassword = CryptoUtils.md5(password);
+
+      final parame =  {
+        "mobile": phoneNumber,
+        "login_system": _loginSystem,
+        // "password": '6730d7b53ea42d2b0b88ae6ba590812b',
+        "password": encryptedPassword,
+        "login_type": _loginType
+      };
+      AppLogger.d('登录请求参数parame: $parame');
+
+      Response response = await DioClient.post<Map<String, dynamic>>(
+        path.join(ApiConstants.baseUrl, ApiConstants.login),
+        data: parame,
+      );
+      if (response.statusCode == 200) {
+          final data = response.data;
+          if (data['code'] == 20000) {
+            AppLogger.d('登录成功: $data');
+            if (mounted) {
+              context.replace('/mainTab');
+            }
+          } else {
+            AppLogger.d('登录失败: $data');
+          }
+      }
     }
   }
 
@@ -89,12 +150,20 @@ class _LoginPageState extends ConsumerState<LoginPage>
       // ScaffoldMessenger.of(context).showSnackBar(
         // SnackBar(content: Text(l10n.loginNotImplemented)),
       // );
-      context.go('/mainTab');
+      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(
@@ -210,32 +279,35 @@ class _LoginPageState extends ConsumerState<LoginPage>
           ),
           const SizedBox(height: 16),
           // 密码输入
-          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,
+          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;
+                    },
+                  ),
                 ),
-                onPressed: () {
-                  setState(() {
-                    _obscurePassword = !_obscurePassword;
-                  });
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return l10n.passwordRequired;
+                  }
+                  if (value.length < AppConstants.passwordMinLength) {
+                    return l10n.passwordMinLength;
+                  }
+                  return null;
                 },
-              ),
-            ),
-            validator: (value) {
-              if (value == null || value.isEmpty) {
-                return l10n.passwordRequired;
-              }
-              if (value.length < 6) {
-                return l10n.passwordMinLength;
-              }
-              return null;
+              );
             },
           ),
           const SizedBox(height: 8),
@@ -311,7 +383,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
                     if (value == null || value.isEmpty) {
                       return l10n.smsCodeRequired;
                     }
-                    if (value.length != 6) {
+                    if (value.length != AppConstants.smsCodeLength) {
                       return l10n.smsCodeLength;
                     }
                     return null;
@@ -319,27 +391,32 @@ class _LoginPageState extends ConsumerState<LoginPage>
                 ),
               ),
               const SizedBox(width: 12),
-              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,
+              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,
+                        ),
+                      ),
                     ),
-                  ),
-                ),
+                  );
+                },
               ),
             ],
           ),

+ 24 - 0
lib/features/auth/presentation/login_provider.dart

@@ -0,0 +1,24 @@
+import 'package:flutter_riverpod/legacy.dart';
+
+/// 登录页面当前选中的 Tab 索引 (0: 密码登录, 1: 验证码登录)
+final loginTabIndexProvider = StateProvider<int>((ref) => 0);
+
+/// 密码登录 - 密码是否可见
+final passwordObscureProvider = StateProvider<bool>((ref) => true);
+
+/// 验证码登录 - 倒计时秒数
+final smsCountdownProvider = StateProvider<int>((ref) => 0);
+
+/// 密码登录 - 手机号
+final passwordLoginPhoneProvider = StateProvider<String>((ref) => '');
+
+/// 密码登录 - 密码
+final passwordLoginPasswordProvider = StateProvider<String>((ref) => '');
+
+/// 验证码登录 - 手机号
+final smsLoginPhoneProvider = StateProvider<String>((ref) => '');
+
+/// 验证码登录 - 验证码
+final smsLoginCodeProvider = StateProvider<String>((ref) => '');
+
+

+ 12 - 19
lib/features/main_tab_page.dart

@@ -1,4 +1,3 @@
-
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 
@@ -6,20 +5,12 @@ import 'HomePage/presentation/home_page.dart';
 import 'HospitalPage/presentation/hospital_page.dart';
 import 'MallPage/presentation/mall_page.dart';
 import 'MinePage/presentation/mine_page.dart';
+import 'main_tab_provider.dart';
 
-class MainTabPage extends ConsumerStatefulWidget{
+class MainTabPage extends ConsumerWidget {
   const MainTabPage({super.key});
 
-  @override
-  ConsumerState<MainTabPage> createState() => _MainTabPageState();
-
-}
-
-class _MainTabPageState extends ConsumerState<MainTabPage>
-    with SingleTickerProviderStateMixin {
-  int _currentIndex = 0;
-
-  final List<Widget> _pages = const [
+  static const List<Widget> _pages = [
     HomePage(),
     HospitalPage(),
     MallPage(),
@@ -27,19 +18,21 @@ class _MainTabPageState extends ConsumerState<MainTabPage>
   ];
 
   @override
-  Widget build(BuildContext context) {
+  Widget build(BuildContext context, WidgetRef ref) {
+    // 使用 ref.watch 监听当前 tab 索引的变化
+    final currentIndex = ref.watch(currentTabIndexProvider);
+
     return Scaffold(
       body: IndexedStack(
-        index: _currentIndex,
+        index: currentIndex,
         children: _pages,
       ),
       bottomNavigationBar: BottomNavigationBar(
-        currentIndex: _currentIndex,
+        currentIndex: currentIndex,
         type: BottomNavigationBarType.fixed,
         onTap: (index) {
-          setState(() {
-            _currentIndex = index;
-          });
+          // 使用 ref.read 更新状态
+          ref.read(currentTabIndexProvider.notifier).state = index;
         },
         items: const [
           BottomNavigationBarItem(
@@ -62,7 +55,7 @@ class _MainTabPageState extends ConsumerState<MainTabPage>
             activeIcon: Icon(Icons.person),
             label: '我的',
           ),
-        ]
+        ],
       ),
     );
   }

+ 6 - 0
lib/features/main_tab_provider.dart

@@ -0,0 +1,6 @@
+import 'package:flutter_riverpod/legacy.dart';
+
+/// 底部导航栏当前索引的 Provider
+final currentTabIndexProvider = StateProvider<int>((ref) => 0);
+
+

+ 6 - 0
lib/l10n/app_localizations.dart

@@ -184,6 +184,12 @@ abstract class AppLocalizations {
   /// **'登录'**
   String get login;
 
+  /// 退出登录按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'退出登录'**
+  String get logout;
+
   /// 登录功能提示
   ///
   /// In zh, this message translates to:

+ 3 - 0
lib/l10n/app_localizations_zh.dart

@@ -54,6 +54,9 @@ class AppLocalizationsZh extends AppLocalizations {
   String get login => '登录';
 
   @override
+  String get logout => '退出登录';
+
+  @override
   String get loginNotImplemented => '登录功能待实现';
 
   @override

+ 4 - 0
lib/l10n/app_zh.arb

@@ -60,6 +60,10 @@
   "@login": {
     "description": "登录按钮"
   },
+  "logout": "退出登录",
+  "@logout": {
+    "description": "退出登录按钮"
+  },
   "loginNotImplemented": "登录功能待实现",
   "@loginNotImplemented": {
     "description": "登录功能提示"

+ 1 - 1
pubspec.lock

@@ -106,7 +106,7 @@ packages:
     source: hosted
     version: "1.15.0"
   crypto:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: crypto
       sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf

+ 1 - 0
pubspec.yaml

@@ -53,6 +53,7 @@ dependencies:
   package_info_plus: ^9.0.0
   intl: ^0.20.2
   logger: ^2.6.2
+  crypto: ^3.0.5
 
 dev_dependencies:
   flutter_test: