Przeglądaj źródła

登录逻辑优化。

PC\19500 3 tygodni temu
rodzic
commit
43fdf7ed8b

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

@@ -21,6 +21,9 @@ class AppConstants {
   /// 用户信息存储 Key
   /// 用户信息存储 Key
   static const String keyUserInfo = 'user_info';
   static const String keyUserInfo = 'user_info';
   
   
+  /// 机构信息存储 Key
+  static const String keyInstitutionInfo = 'institution_info';
+
   /// 语言设置 Key
   /// 语言设置 Key
   static const String keyLanguage = 'app_language';
   static const String keyLanguage = 'app_language';
   
   
@@ -33,7 +36,7 @@ class AppConstants {
   // ==================== 验证码相关 ====================
   // ==================== 验证码相关 ====================
   
   
   /// 验证码长度
   /// 验证码长度
-  static const int smsCodeLength = 6;
+  static const int smsCodeLength = 4;
   
   
   /// 验证码倒计时(秒)
   /// 验证码倒计时(秒)
   static const int smsCodeCountdown = 60;
   static const int smsCodeCountdown = 60;

+ 3 - 3
lib/core/network/interceptors/auth_interceptor.dart

@@ -5,9 +5,9 @@ import 'package:sino_med_cloud/core/storage/local_storage.dart';
 /// 认证拦截器 - 自动添加 Token
 /// 认证拦截器 - 自动添加 Token
 class AuthInterceptor extends Interceptor {
 class AuthInterceptor extends Interceptor {
   @override
   @override
-  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
-    // 获取 Token
-    final token = LocalStorage.getToken();
+  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
+    // 获取 Token(异步,会自动初始化)
+    final token = await LocalStorage.getToken();
     
     
     // 如果存在 Token,添加到请求头
     // 如果存在 Token,添加到请求头
     if (token != null && token.isNotEmpty) {
     if (token != null && token.isNotEmpty) {

+ 155 - 18
lib/core/storage/local_storage.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:convert';
 import 'dart:convert';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:sino_med_cloud/core/constants/app_constants.dart';
 import 'package:sino_med_cloud/core/constants/app_constants.dart';
@@ -5,17 +6,38 @@ import 'package:sino_med_cloud/core/constants/app_constants.dart';
 /// 本地存储管理类
 /// 本地存储管理类
 class LocalStorage {
 class LocalStorage {
   static SharedPreferences? _prefs;
   static SharedPreferences? _prefs;
+  static Completer<void>? _initCompleter;
 
 
   /// 初始化本地存储
   /// 初始化本地存储
   static Future<void> init() async {
   static Future<void> init() async {
-    _prefs ??= await SharedPreferences.getInstance();
+    if (_prefs != null) {
+      return;
+    }
+    _prefs = await SharedPreferences.getInstance();
+    _initCompleter?.complete();
+    _initCompleter = null;
   }
   }
 
 
-  /// 获取 SharedPreferences 实例
-  static SharedPreferences get prefs {
-    if (_prefs == null) {
-      throw Exception('LocalStorage not initialized. Call LocalStorage.init() first.');
+  /// 确保已初始化(自动初始化,如果未初始化则初始化)
+  static Future<void> _ensureInitialized() async {
+    if (_prefs != null) {
+      return;
+    }
+    
+    // 如果正在初始化,等待初始化完成
+    if (_initCompleter != null) {
+      await _initCompleter!.future;
+      return;
     }
     }
+    
+    // 开始初始化
+    _initCompleter = Completer<void>();
+    await init();
+  }
+
+  /// 获取 SharedPreferences 实例(内部使用)
+  static Future<SharedPreferences> get _prefsAsync async {
+    await _ensureInitialized();
     return _prefs!;
     return _prefs!;
   }
   }
 
 
@@ -23,11 +45,13 @@ class LocalStorage {
 
 
   /// 保存字符串
   /// 保存字符串
   static Future<bool> setString(String key, String value) async {
   static Future<bool> setString(String key, String value) async {
+    final prefs = await _prefsAsync;
     return await prefs.setString(key, value);
     return await prefs.setString(key, value);
   }
   }
 
 
   /// 获取字符串
   /// 获取字符串
-  static String? getString(String key, {String? defaultValue}) {
+  static Future<String?> getString(String key, {String? defaultValue}) async {
+    final prefs = await _prefsAsync;
     return prefs.getString(key) ?? defaultValue;
     return prefs.getString(key) ?? defaultValue;
   }
   }
 
 
@@ -35,11 +59,13 @@ class LocalStorage {
 
 
   /// 保存整数
   /// 保存整数
   static Future<bool> setInt(String key, int value) async {
   static Future<bool> setInt(String key, int value) async {
+    final prefs = await _prefsAsync;
     return await prefs.setInt(key, value);
     return await prefs.setInt(key, value);
   }
   }
 
 
   /// 获取整数
   /// 获取整数
-  static int? getInt(String key, {int? defaultValue}) {
+  static Future<int?> getInt(String key, {int? defaultValue}) async {
+    final prefs = await _prefsAsync;
     return prefs.getInt(key) ?? defaultValue;
     return prefs.getInt(key) ?? defaultValue;
   }
   }
 
 
@@ -47,11 +73,13 @@ class LocalStorage {
 
 
   /// 保存浮点数
   /// 保存浮点数
   static Future<bool> setDouble(String key, double value) async {
   static Future<bool> setDouble(String key, double value) async {
+    final prefs = await _prefsAsync;
     return await prefs.setDouble(key, value);
     return await prefs.setDouble(key, value);
   }
   }
 
 
   /// 获取浮点数
   /// 获取浮点数
-  static double? getDouble(String key, {double? defaultValue}) {
+  static Future<double?> getDouble(String key, {double? defaultValue}) async {
+    final prefs = await _prefsAsync;
     return prefs.getDouble(key) ?? defaultValue;
     return prefs.getDouble(key) ?? defaultValue;
   }
   }
 
 
@@ -59,11 +87,13 @@ class LocalStorage {
 
 
   /// 保存布尔值
   /// 保存布尔值
   static Future<bool> setBool(String key, bool value) async {
   static Future<bool> setBool(String key, bool value) async {
+    final prefs = await _prefsAsync;
     return await prefs.setBool(key, value);
     return await prefs.setBool(key, value);
   }
   }
 
 
   /// 获取布尔值
   /// 获取布尔值
-  static bool? getBool(String key, {bool? defaultValue}) {
+  static Future<bool?> getBool(String key, {bool? defaultValue}) async {
+    final prefs = await _prefsAsync;
     return prefs.getBool(key) ?? defaultValue;
     return prefs.getBool(key) ?? defaultValue;
   }
   }
 
 
@@ -71,11 +101,13 @@ class LocalStorage {
 
 
   /// 保存字符串列表
   /// 保存字符串列表
   static Future<bool> setStringList(String key, List<String> value) async {
   static Future<bool> setStringList(String key, List<String> value) async {
+    final prefs = await _prefsAsync;
     return await prefs.setStringList(key, value);
     return await prefs.setStringList(key, value);
   }
   }
 
 
   /// 获取字符串列表
   /// 获取字符串列表
-  static List<String>? getStringList(String key) {
+  static Future<List<String>?> getStringList(String key) async {
+    final prefs = await _prefsAsync;
     return prefs.getStringList(key);
     return prefs.getStringList(key);
   }
   }
 
 
@@ -96,6 +128,7 @@ class LocalStorage {
     try {
     try {
       // 将 Map 转换为 JSON 字符串保存
       // 将 Map 转换为 JSON 字符串保存
       final jsonString = jsonEncode(value);
       final jsonString = jsonEncode(value);
+      final prefs = await _prefsAsync;
       return await prefs.setString(key, jsonString);
       return await prefs.setString(key, jsonString);
     } catch (e) {
     } catch (e) {
       // 如果转换失败,返回 false
       // 如果转换失败,返回 false
@@ -112,11 +145,12 @@ class LocalStorage {
   /// 
   /// 
   /// 示例:
   /// 示例:
   /// ```dart
   /// ```dart
-  /// final user = LocalStorage.getMap('user');
+  /// final user = await LocalStorage.getMap('user');
   /// // 返回:{'name': 'John', 'age': 30}
   /// // 返回:{'name': 'John', 'age': 30}
   /// ```
   /// ```
-  static Map<String, dynamic>? getMap(String key, {Map<String, dynamic>? defaultValue}) {
+  static Future<Map<String, dynamic>?> getMap(String key, {Map<String, dynamic>? defaultValue}) async {
     try {
     try {
+      final prefs = await _prefsAsync;
       final jsonString = prefs.getString(key);
       final jsonString = prefs.getString(key);
       if (jsonString == null) {
       if (jsonString == null) {
         return defaultValue;
         return defaultValue;
@@ -130,25 +164,86 @@ class LocalStorage {
     }
     }
   }
   }
 
 
+  // ==================== List 操作 ====================
+
+  /// 保存列表(List<Map<String, dynamic>>)
+  /// 
+  /// [key] 存储的键
+  /// [value] 要保存的列表对象
+  /// 
+  /// 返回保存是否成功
+  /// 
+  /// 示例:
+  /// ```dart
+  /// await LocalStorage.setList('institutions', [
+  ///   {'id': 10, 'name': '机构1'},
+  ///   {'id': 20, 'name': '机构2'},
+  /// ]);
+  /// ```
+  static Future<bool> setList(String key, List<Map<String, dynamic>> value) async {
+    try {
+      // 将 List 转换为 JSON 字符串保存
+      final jsonString = jsonEncode(value);
+      final prefs = await _prefsAsync;
+      return await prefs.setString(key, jsonString);
+    } catch (e) {
+      // 如果转换失败,返回 false
+      return false;
+    }
+  }
+
+  /// 获取列表(List<Map<String, dynamic>>)
+  /// 
+  /// [key] 存储的键
+  /// [defaultValue] 默认值(可选)
+  /// 
+  /// 返回列表对象,如果不存在或解析失败则返回 null 或默认值
+  /// 
+  /// 示例:
+  /// ```dart
+  /// final institutions = await LocalStorage.getList('institutions');
+  /// // 返回:[{'id': 10, 'name': '机构1'}, {'id': 20, 'name': '机构2'}]
+  /// ```
+  static Future<List<Map<String, dynamic>>?> getList(String key, {List<Map<String, dynamic>>? defaultValue}) async {
+    try {
+      final prefs = await _prefsAsync;
+      final jsonString = prefs.getString(key);
+      if (jsonString == null) {
+        return defaultValue;
+      }
+      // 将 JSON 字符串解析为 List
+      final decoded = jsonDecode(jsonString) as List;
+      // 将 List 中的每个元素转换为 Map<String, dynamic>
+      return decoded.map((item) => item as Map<String, dynamic>).toList();
+    } catch (e) {
+      // 如果解析失败,返回默认值或 null
+      return defaultValue;
+    }
+  }
+
   // ==================== 通用操作 ====================
   // ==================== 通用操作 ====================
 
 
   /// 删除指定 key
   /// 删除指定 key
   static Future<bool> remove(String key) async {
   static Future<bool> remove(String key) async {
+    final prefs = await _prefsAsync;
     return await prefs.remove(key);
     return await prefs.remove(key);
   }
   }
 
 
   /// 清空所有数据
   /// 清空所有数据
   static Future<bool> clear() async {
   static Future<bool> clear() async {
+    final prefs = await _prefsAsync;
     return await prefs.clear();
     return await prefs.clear();
   }
   }
 
 
   /// 检查 key 是否存在
   /// 检查 key 是否存在
-  static bool containsKey(String key) {
+  static Future<bool> containsKey(String key) async {
+    final prefs = await _prefsAsync;
     return prefs.containsKey(key);
     return prefs.containsKey(key);
   }
   }
 
 
   /// 获取所有 key
   /// 获取所有 key
-  static Set<String> getKeys() {
+  static Future<Set<String>> getKeys() async {
+    final prefs = await _prefsAsync;
     return prefs.getKeys();
     return prefs.getKeys();
   }
   }
 
 
@@ -160,8 +255,8 @@ class LocalStorage {
   }
   }
 
 
   /// 获取 Token
   /// 获取 Token
-  static String? getToken() {
-    return getString(AppConstants.keyToken);
+  static Future<String?> getToken() async {
+    return await getString(AppConstants.keyToken);
   }
   }
 
 
   /// 删除 Token
   /// 删除 Token
@@ -175,13 +270,55 @@ class LocalStorage {
   }
   }
 
 
   /// 获取用户信息
   /// 获取用户信息
-  static Map<String, dynamic>? getUserInfo() {
-    return getMap(AppConstants.keyUserInfo);
+  static Future<Map<String, dynamic>?> getUserInfo() async {
+    return await getMap(AppConstants.keyUserInfo);
   }
   }
 
 
   /// 删除用户信息
   /// 删除用户信息
   static Future<bool> removeUserInfo() async {
   static Future<bool> removeUserInfo() async {
     return await remove(AppConstants.keyUserInfo);
     return await remove(AppConstants.keyUserInfo);
   }
   }
+
+  /// 保存机构信息列表
+  ///
+  /// [institutionInfo] 机构信息列表,格式为:
+  /// ```dart
+  /// [
+  ///   {
+  ///     "id": 10,
+  ///     "uuid": "38a0d1fe-c08f-11f0-88d5-0242c0a80106",
+  ///     "name": "仙豆仙豆"
+  ///   }
+  /// ]
+  /// ```
+  ///
+  /// 返回保存是否成功
+  static Future<bool> saveInstitutionInfo(List<Map<String, dynamic>> institutionInfo) async {
+    return await setList(AppConstants.keyInstitutionInfo, institutionInfo);
+  }
+
+  /// 获取机构信息列表
+  ///
+  /// 返回机构信息列表,如果不存在或解析失败则返回 null
+  ///
+  /// 示例:
+  /// ```dart
+  /// final institutionInfo = await LocalStorage.getInstitutionInfo();
+  /// if (institutionInfo != null) {
+  ///   for (var institution in institutionInfo) {
+  ///     print('机构名称: ${institution['name']}');
+  ///   }
+  /// }
+  /// ```
+  static Future<List<Map<String, dynamic>>?> getInstitutionInfo() async {
+    return await getList(AppConstants.keyInstitutionInfo);
+  }
+
+  /// 删除机构信息列表
+  ///
+  /// 返回删除是否成功
+  static Future<bool> removeInstitutionInfo() async {
+    return await remove(AppConstants.keyInstitutionInfo);
+  }
 }
 }
 
 

+ 122 - 0
lib/features/auth/data/auth_model.dart

@@ -0,0 +1,122 @@
+/// 机构信息模型
+class InstitutionInfo {
+  final int id;
+  final String uuid;
+  final String name;
+
+  InstitutionInfo({
+    required this.id,
+    required this.uuid,
+    required this.name,
+  });
+
+  /// 从 JSON 创建 InstitutionInfo
+  factory InstitutionInfo.fromJson(Map<String, dynamic> json) {
+    return InstitutionInfo(
+      id: json['id'] as int,
+      uuid: json['uuid'] as String,
+      name: json['name'] as String,
+    );
+  }
+
+  /// 转换为 JSON
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'uuid': uuid,
+      'name': name,
+    };
+  }
+}
+
+/// 用户信息模型
+class UserInfo {
+  final int id;
+  final String uuid;
+  final String institutionUuid;
+  final String name;
+  final int gender;
+  final String? avatar;
+  final String baseUuid;
+  final String username;
+  final String mobile;
+
+  UserInfo({
+    required this.id,
+    required this.uuid,
+    required this.institutionUuid,
+    required this.name,
+    required this.gender,
+    this.avatar,
+    required this.baseUuid,
+    required this.username,
+    required this.mobile,
+  });
+
+  /// 从 JSON 创建 UserInfo
+  factory UserInfo.fromJson(Map<String, dynamic> json) {
+    return UserInfo(
+      id: json['id'] as int,
+      uuid: json['uuid'] as String,
+      institutionUuid: json['institution_uuid'] as String,
+      name: json['name'] as String,
+      gender: json['gender'] as int,
+      avatar: json['avatar'] as String?,
+      baseUuid: json['base_uuid'] as String,
+      username: json['username'] as String,
+      mobile: json['mobile'] as String,
+    );
+  }
+
+  /// 转换为 JSON
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'uuid': uuid,
+      'institution_uuid': institutionUuid,
+      'name': name,
+      'gender': gender,
+      'avatar': avatar,
+      'base_uuid': baseUuid,
+      'username': username,
+      'mobile': mobile,
+    };
+  }
+}
+
+/// 认证响应模型
+class AuthModel {
+  final String accessToken;
+  final String? refreshToken;
+  final List<InstitutionInfo> institutionInfo;
+  final UserInfo userInfo;
+
+  AuthModel({
+    required this.accessToken,
+    this.refreshToken,
+    required this.institutionInfo,
+    required this.userInfo,
+  });
+
+  /// 从 JSON 创建 AuthModel
+  factory AuthModel.fromJson(Map<String, dynamic> json) {
+    return AuthModel(
+      accessToken: json['access_token'] as String,
+      refreshToken: json['refresh_token'] as String?,
+      institutionInfo: (json['institution_info'] as List)
+          .map((item) => InstitutionInfo.fromJson(item as Map<String, dynamic>))
+          .toList(),
+      userInfo: UserInfo.fromJson(json['user_info'] as Map<String, dynamic>),
+    );
+  }
+
+  /// 转换为 JSON
+  Map<String, dynamic> toJson() {
+    return {
+      'access_token': accessToken,
+      'refresh_token': refreshToken,
+      'institution_info': institutionInfo.map((item) => item.toJson()).toList(),
+      'user_info': userInfo.toJson(),
+    };
+  }
+}

+ 181 - 0
lib/features/auth/domain/login_service.dart

@@ -0,0 +1,181 @@
+import 'package:path/path.dart' as path;
+import 'package:dio/dio.dart';
+import 'package:sino_med_cloud/core/constants/api_constants.dart';
+import 'package:sino_med_cloud/core/network/dio_client.dart';
+import 'package:sino_med_cloud/core/storage/local_storage.dart';
+import 'package:sino_med_cloud/core/utils/logger.dart';
+import '../data/auth_model.dart';
+
+/// 登录服务类
+class LoginService {
+  /// 密码登录
+  ///
+  /// [mobile] 手机号
+  /// [password] 密码(已加密)
+  /// [loginSystem] 登录系统标识
+  /// [loginType] 登录类型
+  ///
+  /// 返回 [AuthModel] 认证模型,如果登录失败则抛出异常
+  static Future<AuthModel> passwordLogin({
+    required String mobile,
+    required String password,
+    required String loginSystem,
+    required String loginType,
+  }) async {
+    try {
+      final parame = {
+        "mobile": mobile,
+        "login_system": loginSystem,
+        "password": password,
+        "login_type": loginType,
+      };
+      AppLogger.d('密码登录请求参数: $parame');
+
+      final 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 != null && data['code'] == 20000) {
+          AppLogger.d('密码登录成功: $data');
+          final jsonData = data['data'] as Map<String, dynamic>;
+          
+          // 解析为 AuthModel
+          final authModel = AuthModel.fromJson(jsonData);
+          
+          // 保存到本地存储
+          await _saveAuthData(authModel);
+          
+          return authModel;
+        } else {
+          final errorMsg = data?['message'] ?? '登录失败';
+          AppLogger.d('密码登录失败: $errorMsg');
+          throw Exception(errorMsg);
+        }
+      } else {
+        AppLogger.d('密码登录请求错误: ${response.statusCode}, ${response.statusMessage}');
+        throw Exception('请求失败: ${response.statusCode}');
+      }
+    } catch (e) {
+      AppLogger.e('密码登录错误', e);
+      rethrow;
+    }
+  }
+
+  /// 发送验证码
+  ///
+  /// [mobile] 手机号
+  /// [scope] 验证码作用域,默认为 "yun-his-login-sms-send"
+  ///
+  /// 返回验证码字符串,如果发送失败则抛出异常
+  static Future<String> sendSmsCode({
+    required String mobile,
+    String scope = "yun-his-login-sms-send",
+  }) async {
+    try {
+      final parame = {
+        "mobile": mobile,
+        "scope": scope,
+      };
+      AppLogger.d('发送验证码请求参数: $parame');
+
+      final response = await DioClient.post<Map<String, dynamic>>(
+        path.join(ApiConstants.baseUrl, ApiConstants.sendSmsCode),
+        data: parame,
+      );
+
+      if (response.statusCode == 200) {
+        final data = response.data;
+        if (data != null && data['code'] == 20000) {
+          AppLogger.d('发送验证码成功: $data');
+          final jsonData = data['data'] as Map<String, dynamic>;
+          final smsCode = jsonData['code'] as String;
+          return smsCode;
+        } else {
+          final errorMsg = data?['message'] ?? '发送验证码失败';
+          AppLogger.d('发送验证码失败: $errorMsg');
+          throw Exception(errorMsg);
+        }
+      } else {
+        AppLogger.d('发送验证码请求错误: ${response.statusCode}, ${response.statusMessage}');
+        throw Exception('请求失败: ${response.statusCode}');
+      }
+    } catch (e) {
+      AppLogger.e('发送验证码错误', e);
+      rethrow;
+    }
+  }
+
+  /// 验证码登录
+  ///
+  /// [mobile] 手机号
+  /// [smsCode] 验证码
+  /// [loginSystem] 登录系统标识
+  /// [loginType] 登录类型
+  ///
+  /// 返回 [AuthModel] 认证模型,如果登录失败则抛出异常
+  static Future<AuthModel> smsLogin({
+    required String mobile,
+    required String smsCode,
+    required String loginSystem,
+    required String loginType,
+  }) async {
+    try {
+      final parame = {
+        "mobile": mobile,
+        "sms_code": smsCode,
+        "login_system": loginSystem,
+        "login_type": loginType,
+      };
+      AppLogger.d('验证码登录请求参数: $parame');
+
+      final 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 != null && data['code'] == 20000) {
+          AppLogger.d('验证码登录成功: $data');
+          final jsonData = data['data'] as Map<String, dynamic>;
+          
+          // 解析为 AuthModel
+          final authModel = AuthModel.fromJson(jsonData);
+          
+          // 保存到本地存储
+          await _saveAuthData(authModel);
+          
+          return authModel;
+        } else {
+          final errorMsg = data?['message'] ?? '登录失败';
+          AppLogger.d('验证码登录失败: $errorMsg');
+          throw Exception(errorMsg);
+        }
+      } else {
+        AppLogger.d('验证码登录请求错误: ${response.statusCode}, ${response.statusMessage}');
+        throw Exception('请求失败: ${response.statusCode}');
+      }
+    } catch (e) {
+      AppLogger.e('验证码登录错误', e);
+      rethrow;
+    }
+  }
+
+  /// 保存认证数据到本地存储
+  static Future<void> _saveAuthData(AuthModel authModel) async {
+    // 保存 Token(会自动初始化)
+    await LocalStorage.saveToken(authModel.accessToken);
+    
+    // 保存用户信息(会自动初始化)
+    await LocalStorage.saveUserInfo(authModel.userInfo.toJson());
+    
+    // 保存机构信息列表(会自动初始化)
+    final institutionInfoList = authModel.institutionInfo
+        .map((item) => item.toJson())
+        .toList();
+    await LocalStorage.saveInstitutionInfo(institutionInfoList);
+  }
+}

+ 138 - 56
lib/features/auth/presentation/login_page.dart

@@ -1,16 +1,13 @@
+import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.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/l10n/app_localizations.dart';
 import 'package:sino_med_cloud/core/constants/app_constants.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/logger.dart';
 import '../../../core/utils/crypto_utils.dart';
 import '../../../core/utils/crypto_utils.dart';
+import '../domain/login_service.dart';
 import 'login_provider.dart';
 import 'login_provider.dart';
-import 'package:dio/dio.dart';
 
 
 class LoginPage extends ConsumerStatefulWidget {
 class LoginPage extends ConsumerStatefulWidget {
   const LoginPage({super.key});
   const LoginPage({super.key});
@@ -29,6 +26,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
   //用于test
   //用于test
   final _loginSystem = "YUN_HIS_PC_WEB";
   final _loginSystem = "YUN_HIS_PC_WEB";
   final _loginType = "MOBILE_PASSWORD";
   final _loginType = "MOBILE_PASSWORD";
+  final _loginSmsType = "MOBILE_SMS_CODE";
 
 
   // 密码登录表单
   // 密码登录表单
   final _phoneController = TextEditingController();
   final _phoneController = TextEditingController();
@@ -37,6 +35,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
   // 验证码登录表单
   // 验证码登录表单
   final _phoneSmsController = TextEditingController();
   final _phoneSmsController = TextEditingController();
   final _smsCodeController = TextEditingController();
   final _smsCodeController = TextEditingController();
+  Timer? _countdownTimer;
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -75,11 +74,18 @@ class _LoginPageState extends ConsumerState<LoginPage>
     _passwordController.dispose();
     _passwordController.dispose();
     _phoneSmsController.dispose();
     _phoneSmsController.dispose();
     _smsCodeController.dispose();
     _smsCodeController.dispose();
+    _clearCountdownTimer();
     super.dispose();
     super.dispose();
   }
   }
 
 
+  /// 清理倒计时定时器
+  void _clearCountdownTimer() {
+    _countdownTimer?.cancel();
+    _countdownTimer = null;
+  }
+
   // 发送验证码
   // 发送验证码
-  void _sendSmsCode() {
+  void _sendSmsCode() async {
     final l10n = AppLocalizations.of(context)!;
     final l10n = AppLocalizations.of(context)!;
     final phone = ref.read(smsLoginPhoneProvider);
     final phone = ref.read(smsLoginPhoneProvider);
     if (phone.isEmpty) {
     if (phone.isEmpty) {
@@ -89,21 +95,50 @@ class _LoginPageState extends ConsumerState<LoginPage>
       return;
       return;
     }
     }
 
 
-    // TODO: 调用发送验证码接口
-    // 开始倒计时
-    ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown;
+    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;
 
 
-    // 倒计时
-    Future.doWhile(() async {
-      await Future.delayed(const Duration(seconds: 1));
-      if (mounted) {
+      // 使用 Timer 进行倒计时
+      _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
+        if (!mounted) {
+          timer.cancel();
+          return;
+        }
+        
         final currentCountdown = ref.read(smsCountdownProvider);
         final currentCountdown = ref.read(smsCountdownProvider);
-        if (currentCountdown > 0) {
+        if (currentCountdown > 1) {
           ref.read(smsCountdownProvider.notifier).state = 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) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text(e.toString())),
+        );
       }
       }
-      return ref.read(smsCountdownProvider) > 0;
-    });
+    }
   }
   }
 
 
   // 密码登录
   // 密码登录
@@ -117,54 +152,88 @@ class _LoginPageState extends ConsumerState<LoginPage>
         // 对密码进行 MD5 加密
         // 对密码进行 MD5 加密
         final encryptedPassword = CryptoUtils.md5(password);
         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<Map<String, dynamic>>(
-          path.join(ApiConstants.baseUrl, ApiConstants.login),
-          data: parame,
+        // 调用登录服务
+        await LoginService.passwordLogin(
+          mobile: phoneNumber,
+          password: encryptedPassword,
+          loginSystem: _loginSystem,
+          loginType: _loginType,
         );
         );
-        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}');
+
+        // 登录成功,跳转到主页
+        if (mounted) {
+          context.replace('/mainTab');
         }
         }
       }
       }
     } catch (e) {
     } catch (e) {
-      AppLogger.e('密码登录错误:handlePasswordLogin', e);
-      rethrow;
+      AppLogger.e('密码登录错误', e);
+      if (mounted) {
+        final l10n = AppLocalizations.of(context)!;
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text(e.toString())),
+        );
+      }
     }
     }
   }
   }
 
 
   // 验证码登录
   // 验证码登录
-  void _handleSmsLogin() {
-    final l10n = AppLocalizations.of(context)!;
-    if (_smsFormKey.currentState!.validate()) {
-      // TODO: 调用登录接口
-      // ScaffoldMessenger.of(context).showSnackBar(
-        // SnackBar(content: Text(l10n.loginNotImplemented)),
-      // );
-      context.push('/mainTab');
+  void _handleSmsLogin() async {
+    try {
+      final l10n = AppLocalizations.of(context)!;
+      
+      if (_smsFormKey.currentState!.validate()) {
+        final smsCode = ref.watch(smsLoginCodeProvider);
+        final serverSmsCode = ref.read(smsCodeFromServerProvider);
+        
+        // 检查验证码是否已过期(为空或倒计时已结束)
+        if (serverSmsCode.isEmpty) {
+          ScaffoldMessenger.of(context).showSnackBar(
+            SnackBar(content: Text(l10n.smsCodeHasExpired)),
+          );
+          return;
+        }
+        
+        // 验证码输入错误
+        if (smsCode != serverSmsCode) {
+          ScaffoldMessenger.of(context).showSnackBar(
+            SnackBar(content: Text(l10n.smsCodeError)),
+          );
+          return;
+        }
+        
+        final phoneNumber = ref.watch(smsLoginPhoneProvider);
+
+        // 调用登录服务
+        await LoginService.smsLogin(
+          mobile: phoneNumber,
+          smsCode: smsCode,
+          loginSystem: _loginSystem,
+          loginType: _loginSmsType,
+        );
+
+        // 登录成功,清理倒计时和定时器
+        _clearCountdownTimer();
+        ref.read(smsCountdownProvider.notifier).state = 0;
+        ref.read(smsHasReceivedProvider.notifier).state = false;
+        ref.read(smsCodeFromServerProvider.notifier).state = '';
+
+        // 跳转到主页
+        if (mounted) {
+          context.replace('/mainTab');
+        }
+      }
+    } catch (e) {
+      AppLogger.e('验证码登录错误', e);
+      if (mounted) {
+        final l10n = AppLocalizations.of(context)!;
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text(e.toString())),
+        );
+      }
     }
     }
   }
   }
 
 
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     // 设置监听器(只设置一次)
     // 设置监听器(只设置一次)
@@ -391,11 +460,13 @@ class _LoginPageState extends ConsumerState<LoginPage>
                     prefixIcon: const Icon(Icons.sms_outlined),
                     prefixIcon: const Icon(Icons.sms_outlined),
                   ),
                   ),
                   validator: (value) {
                   validator: (value) {
+                    // 验证码为空
                     if (value == null || value.isEmpty) {
                     if (value == null || value.isEmpty) {
                       return l10n.smsCodeRequired;
                       return l10n.smsCodeRequired;
                     }
                     }
-                    if (value.length != AppConstants.smsCodeLength) {
-                      return l10n.smsCodeLength;
+                    // 验证码格式不正确(长度或格式)
+                    if (value.length != AppConstants.smsCodeLength || !RegExp(r'^\d+$').hasMatch(value)) {
+                      return l10n.smsCodeInvalid;
                     }
                     }
                     return null;
                     return null;
                   },
                   },
@@ -405,6 +476,16 @@ class _LoginPageState extends ConsumerState<LoginPage>
               Consumer(
               Consumer(
                 builder: (context, ref, child) {
                 builder: (context, ref, child) {
                   final countdown = ref.watch(smsCountdownProvider);
                   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(
                   return SizedBox(
                     width: 100,
                     width: 100,
                     child: ElevatedButton(
                     child: ElevatedButton(
@@ -418,11 +499,12 @@ class _LoginPageState extends ConsumerState<LoginPage>
                       child: Text(
                       child: Text(
                         countdown > 0
                         countdown > 0
                             ? l10n.smsCodeCountdown(countdown)
                             ? l10n.smsCodeCountdown(countdown)
-                            : l10n.getSmsCode,
+                            : (isExpired ? l10n.resendSmsCode : l10n.getSmsCode),
                         style: TextStyle(
                         style: TextStyle(
                           color: countdown > 0
                           color: countdown > 0
                               ? const Color(0xFF6B7280)
                               ? const Color(0xFF6B7280)
                               : Colors.white,
                               : Colors.white,
+                          fontSize: isExpired ? 12 : 14,
                         ),
                         ),
                       ),
                       ),
                     ),
                     ),

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

@@ -9,6 +9,12 @@ final passwordObscureProvider = StateProvider<bool>((ref) => true);
 /// 验证码登录 - 倒计时秒数
 /// 验证码登录 - 倒计时秒数
 final smsCountdownProvider = StateProvider<int>((ref) => 0);
 final smsCountdownProvider = StateProvider<int>((ref) => 0);
 
 
+/// 验证码登录 - 是否曾经获取过验证码
+final smsHasReceivedProvider = StateProvider<bool>((ref) => false);
+
+/// 验证码登录 - 服务器返回的验证码
+final smsCodeFromServerProvider = StateProvider<String>((ref) => '');
+
 /// 密码登录 - 手机号
 /// 密码登录 - 手机号
 final passwordLoginPhoneProvider = StateProvider<String>((ref) => '');
 final passwordLoginPhoneProvider = StateProvider<String>((ref) => '');
 
 

+ 31 - 1
lib/l10n/app_localizations.dart

@@ -217,15 +217,45 @@ abstract class AppLocalizations {
   /// 验证码长度验证
   /// 验证码长度验证
   ///
   ///
   /// In zh, this message translates to:
   /// In zh, this message translates to:
-  /// **'验证码为6位数字'**
+  /// **'验证码为4位数字'**
   String get smsCodeLength;
   String get smsCodeLength;
 
 
+  /// 验证码格式验证
+  ///
+  /// In zh, this message translates to:
+  /// **'验证码格式不正确'**
+  String get smsCodeInvalid;
+
+  /// 验证码已过期或未获取提示
+  ///
+  /// In zh, this message translates to:
+  /// **'请获取验证码'**
+  String get smsCodeExpired;
+
+  /// 验证码过期提示
+  ///
+  /// In zh, this message translates to:
+  /// **'验证码已过期'**
+  String get smsCodeHasExpired;
+
+  /// 验证码输入错误提示
+  ///
+  /// In zh, this message translates to:
+  /// **'验证码错误'**
+  String get smsCodeError;
+
   /// 获取验证码按钮
   /// 获取验证码按钮
   ///
   ///
   /// In zh, this message translates to:
   /// In zh, this message translates to:
   /// **'获取验证码'**
   /// **'获取验证码'**
   String get getSmsCode;
   String get getSmsCode;
 
 
+  /// 重新获取验证码按钮文本
+  ///
+  /// In zh, this message translates to:
+  /// **'请重新获取验证码'**
+  String get resendSmsCode;
+
   /// 验证码倒计时
   /// 验证码倒计时
   ///
   ///
   /// In zh, this message translates to:
   /// In zh, this message translates to:

+ 16 - 1
lib/l10n/app_localizations_zh.dart

@@ -69,12 +69,27 @@ class AppLocalizationsZh extends AppLocalizations {
   String get smsCodeRequired => '请输入验证码';
   String get smsCodeRequired => '请输入验证码';
 
 
   @override
   @override
-  String get smsCodeLength => '验证码为6位数字';
+  String get smsCodeLength => '验证码为4位数字';
+
+  @override
+  String get smsCodeInvalid => '验证码格式不正确';
+
+  @override
+  String get smsCodeExpired => '请获取验证码';
+
+  @override
+  String get smsCodeHasExpired => '验证码已过期';
+
+  @override
+  String get smsCodeError => '验证码错误';
 
 
   @override
   @override
   String get getSmsCode => '获取验证码';
   String get getSmsCode => '获取验证码';
 
 
   @override
   @override
+  String get resendSmsCode => '请重新获取验证码';
+
+  @override
   String smsCodeCountdown(int count) {
   String smsCodeCountdown(int count) {
     return '$count秒';
     return '$count秒';
   }
   }

+ 21 - 1
lib/l10n/app_zh.arb

@@ -80,11 +80,31 @@
   "@smsCodeRequired": {
   "@smsCodeRequired": {
     "description": "验证码必填验证"
     "description": "验证码必填验证"
   },
   },
-  "smsCodeLength": "验证码为6位数字",
+  "smsCodeLength": "验证码为4位数字",
   "@smsCodeLength": {
   "@smsCodeLength": {
     "description": "验证码长度验证"
     "description": "验证码长度验证"
   },
   },
+  "smsCodeInvalid": "验证码格式不正确",
+  "@smsCodeInvalid": {
+    "description": "验证码格式验证"
+  },
+  "smsCodeExpired": "请获取验证码",
+  "@smsCodeExpired": {
+    "description": "验证码已过期或未获取提示"
+  },
+  "smsCodeHasExpired": "验证码已过期",
+  "@smsCodeHasExpired": {
+    "description": "验证码过期提示"
+  },
+  "smsCodeError": "验证码错误",
+  "@smsCodeError": {
+    "description": "验证码输入错误提示"
+  },
   "getSmsCode": "获取验证码",
   "getSmsCode": "获取验证码",
+  "resendSmsCode": "请重新获取验证码",
+  "@resendSmsCode": {
+    "description": "重新获取验证码按钮文本"
+  },
   "@getSmsCode": {
   "@getSmsCode": {
     "description": "获取验证码按钮"
     "description": "获取验证码按钮"
   },
   },

+ 4 - 1
lib/main.dart

@@ -1,7 +1,10 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'app/app.dart';
 import 'app/app.dart';
+import 'core/storage/local_storage.dart';
+
+void main() async {
+  WidgetsFlutterBinding.ensureInitialized();
 
 
-void main() {
   runApp(const ProviderScope(child: SinoMedApp()));
   runApp(const ProviderScope(child: SinoMedApp()));
 }
 }