diff --git a/lib/api/api_path.dart b/lib/api/api_path.dart index 3b602a3..8be3538 100644 --- a/lib/api/api_path.dart +++ b/lib/api/api_path.dart @@ -1,3 +1,3 @@ class ApiPath { - static const String login = "/auth/login"; + static const String sendValidationCode = "/v1/common/sendValidationCode"; } diff --git a/lib/api/api_response.dart b/lib/api/api_response.dart index cd85dfe..007fa8f 100644 --- a/lib/api/api_response.dart +++ b/lib/api/api_response.dart @@ -1,51 +1,70 @@ class ApiResponse { - final bool success; + final String? description; final T? data; - final String? message; - final int? statusCode; + final String? errorMsg; + final int? errorCode; - // 构造函数 const ApiResponse({ - required this.success, + this.description, this.data, - this.message, - this.statusCode, + this.errorMsg, + this.errorCode, }); - // 成功响应 - factory ApiResponse.success(T data, - {String message = 'Success', int? statusCode}) { + // ✅ 新增:从 JSON 创建 ApiResponse + factory ApiResponse.fromJson( + Map json, + T Function(dynamic)? dataFromJson, // 可为空,有些接口 data 是 null + ) { + final dataJson = json['data']; + final T? parsedData = dataJson != null && dataFromJson != null + ? dataFromJson(dataJson) + : null; + + return ApiResponse( + errorCode: json['errorCode'], + errorMsg: json['errorMsg'], + description: json['description'], + data: parsedData, + ); + } + + // 成功工厂构造 + factory ApiResponse.success(T data, { + String? errorMsg, + int? errorCode, + String? description, + }) { return ApiResponse( - success: true, data: data, - message: message, - statusCode: statusCode, + errorMsg: errorMsg ?? 'success', + errorCode: errorCode, + description: description ?? 'success', ); } - // 失败响应 - factory ApiResponse.error(String message, {int? statusCode, T? data}) { + // 失败工厂构造 + factory ApiResponse.error( + String errorMsg, { + int? errorCode, + T? data, + String? description, + }) { return ApiResponse( - success: false, - message: message, - statusCode: statusCode, - data: data, // 可选:返回部分数据(如错误时的缓存数据) + description: description, + errorMsg: errorMsg, + errorCode: errorCode, + data: data, ); } - // 加载中(可选) - factory ApiResponse.loading() { - return ApiResponse( - success: false, - message: 'Loading...', - ); + bool get isSuccess { + return errorCode == 0; } - @override - List get props => [success, data, message, statusCode]; @override String toString() { - return 'ApiResponse(success: $success, message: $message, data: $data, statusCode: $statusCode)'; + return 'ApiResponse(description: $description, errorMsg: $errorMsg, data: $data, errorCode: $errorCode)'; } -} +} \ No newline at end of file diff --git a/lib/api/base_api_service.dart b/lib/api/base_api_service.dart index 8cdc46d..4e393cb 100644 --- a/lib/api/base_api_service.dart +++ b/lib/api/base_api_service.dart @@ -1,7 +1,9 @@ -// api/service/user_api_service.dart import 'package:dio/dio.dart' as dioAlias; + import 'package:starwork_flutter/api/api_response.dart'; +import 'package:starwork_flutter/common/constant/http_constant.dart'; import 'package:starwork_flutter/flavors.dart'; +import 'package:flutter/foundation.dart'; // 用于 debugPrint class BaseApiService { final dioAlias.Dio dio = dioAlias.Dio(); @@ -10,37 +12,62 @@ class BaseApiService { dio.options.baseUrl = F.apiHost; dio.options.connectTimeout = const Duration(seconds: 30); dio.options.receiveTimeout = const Duration(seconds: 30); - // 可添加拦截器、token 等 + + // 🔥 添加日志拦截器 + if (kDebugMode) { + dio.interceptors.add(dioAlias.LogInterceptor( + request: true, + requestHeader: true, + requestBody: true, + responseHeader: true, + responseBody: true, + error: true, + logPrint: (obj) { + debugPrint('[DIO] $obj'); + }, + )); + } } - /// 统一请求方法(私有,但子类可用) + /// 统一请求方法 Future> makeRequest({ required String path, - String method = 'GET', + String method = HttpConstant.post, dynamic data, Map? queryParameters, required T Function(dynamic) fromJson, }) async { try { + // 🔍 打印请求信息(更详细控制) + if (kDebugMode) { + final uri = Uri.parse('${dio.options.baseUrl}$path'); + final queryString = + queryParameters != null ? '?${uri.queryParameters.toString()}' : ''; + debugPrint('🟦 API Request: $method ${uri.toString()}$queryString'); + if (data != null) { + debugPrint('🟦 Request Body: $data'); + } + } + dioAlias.Response response; switch (method.toUpperCase()) { - case 'GET': + case HttpConstant.get: response = await dio.get(path, queryParameters: queryParameters); break; - case 'POST': + case HttpConstant.post: response = await dio.post(path, data: data, queryParameters: queryParameters); break; - case 'PUT': + case HttpConstant.put: response = await dio.put(path, data: data, queryParameters: queryParameters); break; - case 'DELETE': + case HttpConstant.delete: response = await dio.delete(path, data: data, queryParameters: queryParameters); break; - case 'PATCH': + case HttpConstant.patch: response = await dio.patch(path, data: data, queryParameters: queryParameters); break; @@ -48,20 +75,46 @@ class BaseApiService { return ApiResponse.error('Unsupported method: $method'); } + // ✅ 打印响应 + if (kDebugMode) { + debugPrint( + '🟩 API Response [${response.statusCode}] ${response.requestOptions.path}'); + debugPrint('🟩 Response Data: ${response.data}'); + } + if (response.statusCode! >= 200 && response.statusCode! < 300) { - final parsedData = fromJson(response.data); - return ApiResponse.success( - parsedData, - statusCode: response.statusCode, - message: 'Success', - ); + final jsonMap = response.data as Map; + + // ✅ 直接用 ApiResponse.fromJson 解析整个响应 + final apiResponse = ApiResponse.fromJson(jsonMap, fromJson); + + // ✅ 判断业务是否成功(根据 errorCode) + if (apiResponse.errorCode == 0) { + return apiResponse; // 直接返回,data 已解析 + } else { + // 业务失败,但 data 可能有部分信息 + return ApiResponse.error( + apiResponse.errorMsg ?? 'Request failed', + errorCode: apiResponse.errorCode, + data: apiResponse.data, + ); + } } else { return ApiResponse.error( response.statusMessage ?? 'Request failed', - statusCode: response.statusCode, + errorCode: response.statusCode, ); } } on dioAlias.DioException catch (e) { + // ❌ 打印错误 + if (kDebugMode) { + debugPrint('🟥 API Error: ${e.type} - ${e.message}'); + if (e.response != null) { + debugPrint('🟥 Error Response: ${e.response?.data}'); + debugPrint('🟥 Status Code: ${e.response?.statusCode}'); + } + } + String message = 'Unknown error'; int? statusCode = e.response?.statusCode; @@ -77,8 +130,11 @@ class BaseApiService { message = e.message ?? 'Something went wrong'; } - return ApiResponse.error(message, statusCode: statusCode); + return ApiResponse.error(message, errorCode: statusCode); } catch (e) { + if (kDebugMode) { + debugPrint('🟥 Unexpected Error: $e'); + } return ApiResponse.error('Unexpected error: $e'); } } diff --git a/lib/api/model/common/request/send_validation_code_request.dart b/lib/api/model/common/request/send_validation_code_request.dart new file mode 100644 index 0000000..1c77d38 --- /dev/null +++ b/lib/api/model/common/request/send_validation_code_request.dart @@ -0,0 +1,26 @@ +// 验证码接口请求参数 +// 登陆后可使用 sendValidationCodeAuth 接口,免图片滑动验证,只需要 channel,codeType 两个参数即可 +import 'package:starwork_flutter/common/constant/validation_code_type.dart'; + +class SendValidationCodeRequest { + final String countryCode; + final String account; + final String channel; //1 短信,2 邮箱 + final ValidationCodeType codeType; + + SendValidationCodeRequest({ + required this.countryCode, + required this.account, + required this.channel, + required this.codeType, + }); + + Map toJson() { + return { + 'countryCode': countryCode, + 'account': account, + 'channel': channel, + 'codeType': codeType.value, + }; + } +} diff --git a/lib/api/model/string_response.dart b/lib/api/model/string_response.dart new file mode 100644 index 0000000..7091e03 --- /dev/null +++ b/lib/api/model/string_response.dart @@ -0,0 +1,18 @@ +// models/string_response.dart +class StringResponse { + final String value; + + StringResponse(this.value); + + factory StringResponse.fromJson(dynamic data) { + if (data is String) { + return StringResponse(data); + } else if (data is Map && data['message'] != null) { + return StringResponse(data['message'].toString()); + } else if (data is Map && data['data'] != null) { + return StringResponse(data['data'].toString()); + } else { + return StringResponse(data.toString()); + } + } +} \ No newline at end of file diff --git a/lib/api/model/user/request/validation_code_login.dart b/lib/api/model/user/request/validation_code_login.dart new file mode 100644 index 0000000..f291983 --- /dev/null +++ b/lib/api/model/user/request/validation_code_login.dart @@ -0,0 +1,19 @@ +class ValidationCodeLoginRequest { + final int platId; //平台 1web 2app 3小程序 4pc + final String phone; // 登录账号(手机号) + final String verificationCode; //验证码 + + ValidationCodeLoginRequest({ + required this.platId, + required this.phone, + required this.verificationCode, + }); + + Map toJson() { + return { + 'platId': platId, + 'phone': phone, + 'verificationCode': verificationCode, + }; + } +} diff --git a/lib/api/service/common_api_service.dart b/lib/api/service/common_api_service.dart new file mode 100644 index 0000000..9ee9076 --- /dev/null +++ b/lib/api/service/common_api_service.dart @@ -0,0 +1,25 @@ +import 'package:starwork_flutter/api/api_path.dart'; +import 'package:starwork_flutter/api/api_response.dart'; +import 'package:starwork_flutter/api/base_api_service.dart'; +import 'package:starwork_flutter/api/model/common/request/send_validation_code_request.dart'; +import 'package:starwork_flutter/api/model/string_response.dart'; +import 'package:starwork_flutter/api/model/user/user_model.dart'; +import 'package:starwork_flutter/common/constant/http_constant.dart'; + +class CommonApiService { + final BaseApiService _api; + + CommonApiService(this._api); // 通过构造函数注入 + // 发送验证码 + Future> sendValidationCode({ + required SendValidationCodeRequest request, + }) { + return _api.makeRequest( + // 通过实例调用 + path: ApiPath.sendValidationCode, + method: HttpConstant.post, + data: request.toJson(), + fromJson: (data) {}, + ); + } +} diff --git a/lib/api/service/user_api_service.dart b/lib/api/service/user_api_service.dart index e3491e0..8376059 100644 --- a/lib/api/service/user_api_service.dart +++ b/lib/api/service/user_api_service.dart @@ -4,7 +4,9 @@ import 'package:starwork_flutter/api/base_api_service.dart'; import 'package:starwork_flutter/api/model/user/user_model.dart'; class UserApiService { - final BaseApiService _api = BaseApiService(); + final BaseApiService _api; + + UserApiService(this._api); // 通过构造函数注入 Future> login(Map userData) { return _api.makeRequest( diff --git a/lib/app.dart b/lib/app.dart index 0a57d6e..abb4bbb 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -19,12 +19,11 @@ class App extends StatelessWidget { builder: (_, child) { return GetMaterialApp( theme: ThemeData( - textSelectionTheme: TextSelectionThemeData( - cursorColor: Colors.blue.shade200, - selectionColor: Colors.blue.shade100, - selectionHandleColor: Colors.blue.shade300, - ) - ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: Colors.blue.shade200, + selectionColor: Colors.blue.shade100, + selectionHandleColor: Colors.blue.shade300, + )), // 必须配置以下三个本地化代理 localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, // 提供Material组件本地化字符串 @@ -42,7 +41,7 @@ class App extends StatelessWidget { fallbackLocale: const Locale('zh', 'CN'), // 必须设置 fallback 没有该语言时使用 getPages: AppPages.pages, - initialRoute: AppRoutes.main, + initialRoute: AppRoutes.login, debugShowCheckedModeBanner: false, ); }, diff --git a/lib/base/app_initialization.dart b/lib/base/app_initialization.dart index 684e53f..cd806f3 100644 --- a/lib/base/app_initialization.dart +++ b/lib/base/app_initialization.dart @@ -5,20 +5,31 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:starwork_flutter/api/base_api_service.dart'; +import 'package:starwork_flutter/api/service/common_api_service.dart'; import 'package:starwork_flutter/api/service/user_api_service.dart'; import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart'; import 'package:starwork_flutter/i18n/app_i18n.dart'; class AppInitialization { static Future initializeApp() async { - // 确保绑定已初始化,能够安全使用插件 - WidgetsFlutterBinding.ensureInitialized(); - // 设置系统状态栏 - setSystemStatusBar(); - // 初始化 SharedPreferences - await SharedPreferencesUtils.init(); - // 初始化api服务(单例) - Get.put(BaseApiService(),permanent: true); + try { + WidgetsFlutterBinding.ensureInitialized(); + setSystemStatusBar(); + await SharedPreferencesUtils.init(); + + // 日志:方便调试 + print('✅ SharedPreferences initialized'); + + Get.lazyPut(() => BaseApiService()); + Get.lazyPut(() => CommonApiService(Get.find())); + + print('✅ API services registered'); + } catch (e, stack) { + print('❌ Initialization failed: $e'); + print(stack); + // 可以上报错误(Sentry 等) + rethrow; + } } static void setSystemStatusBar() { diff --git a/lib/common/constant/http_constant.dart b/lib/common/constant/http_constant.dart new file mode 100644 index 0000000..44193f3 --- /dev/null +++ b/lib/common/constant/http_constant.dart @@ -0,0 +1,8 @@ +class HttpConstant { + static const String post="POST"; + static const String get="GET"; + static const String put="PUT"; + static const String delete="DELETE"; + static const String patch="PATCH"; + +} \ No newline at end of file diff --git a/lib/common/constant/login_type.dart b/lib/common/constant/login_type.dart new file mode 100644 index 0000000..2d5f934 --- /dev/null +++ b/lib/common/constant/login_type.dart @@ -0,0 +1,34 @@ +class LoginType { + static const phoneCodeLogin = LoginType('1', '手机验证码'); + static const emailCodeLogin = LoginType('2', '邮箱验证码'); + static const emailPasswordLogin = LoginType('3', '邮箱验证码'); + static const phonePassword = LoginType('4', '手机号密码'); + + final String value; + final String label; + + const LoginType(this.value, this.label); + + // 支持通过字符串值查找枚举实例 + static LoginType? fromValue(String? value) { + return { + '1': phoneCodeLogin, + '2': phonePassword, + }[value]; + } + + // 支持 toString() 直接输出 value + @override + String toString() => value; + + // 可选:支持 == 比较 + @override + bool operator ==(Object other) => + identical(this, other) || + other is LoginType && + runtimeType == other.runtimeType && + value == other.value; + + @override + int get hashCode => value.hashCode; +} diff --git a/lib/common/constant/validation_code_channel.dart b/lib/common/constant/validation_code_channel.dart new file mode 100644 index 0000000..5b13b93 --- /dev/null +++ b/lib/common/constant/validation_code_channel.dart @@ -0,0 +1,33 @@ +// Description: 验证码渠道枚举类 +class ValidationCodeChannel { + static const sms = ValidationCodeChannel('1', '短信'); + static const email = ValidationCodeChannel('2', '邮箱'); + + final String value; + final String label; + + const ValidationCodeChannel(this.value, this.label); + + // 支持通过字符串值查找枚举实例 + static ValidationCodeChannel? fromValue(String? value) { + return { + '1': sms, + '2': email, + }[value]; + } + + // 支持 toString() 直接输出 value + @override + String toString() => value; + + // 可选:支持 == 比较 + @override + bool operator ==(Object other) => + identical(this, other) || + other is ValidationCodeChannel && + runtimeType == other.runtimeType && + value == other.value; + + @override + int get hashCode => value.hashCode; +} diff --git a/lib/common/constant/validation_code_type.dart b/lib/common/constant/validation_code_type.dart new file mode 100644 index 0000000..2eff7bd --- /dev/null +++ b/lib/common/constant/validation_code_type.dart @@ -0,0 +1,46 @@ +class ValidationCodeType { + static const login = ValidationCodeType('1', '登录'); + static const reset = ValidationCodeType('2', '重置密码'); + static const bindPhone = ValidationCodeType('3', '绑定手机号'); + static const unbindPhone = ValidationCodeType('4', '解绑手机号'); + static const deleteAccount = ValidationCodeType('5', '删除账号'); + static const bindEmail = ValidationCodeType('6', '绑定邮箱'); + static const unbindEmail = ValidationCodeType('7', '解绑邮箱'); + static const deleteLock = ValidationCodeType('8', '删除门锁'); + static const updatePassword = ValidationCodeType('9', '修改密码'); + + final String value; + final String label; + + const ValidationCodeType(this.value, this.label); + + // 支持通过字符串值查找枚举实例 + static ValidationCodeType? fromValue(String? value) { + return { + '1': login, + '2': reset, + '3': bindPhone, + '4': unbindPhone, + '5': deleteAccount, + '6': bindEmail, + '7': unbindEmail, + '8': deleteLock, + '9': updatePassword, + }[value]; + } + + // 支持 toString() 直接输出 value + @override + String toString() => value; + + // 可选:支持 == 比较 + @override + bool operator ==(Object other) => + identical(this, other) || + other is ValidationCodeType && + runtimeType == other.runtimeType && + value == other.value; + + @override + int get hashCode => value.hashCode; +} diff --git a/lib/common/enums/login_type.dart b/lib/common/enums/login_type.dart deleted file mode 100644 index 5255818..0000000 --- a/lib/common/enums/login_type.dart +++ /dev/null @@ -1,5 +0,0 @@ -enum LoginType { - phoneCode, - phonePassword; - -} \ No newline at end of file diff --git a/lib/flavors.dart b/lib/flavors.dart index b3abb83..99d932f 100644 --- a/lib/flavors.dart +++ b/lib/flavors.dart @@ -32,7 +32,7 @@ class F { static String get apiHost { switch (appFlavor) { case Flavor.skyDev: - return 'https://loacl.work.star-lock.cn/api'; + return 'http://192.168.1.136/api'; case Flavor.skyPre: return 'https://loacl.work.star-lock.cn/api'; case Flavor.skyRelease: diff --git a/lib/views/login/login_controller.dart b/lib/views/login/login_controller.dart index 09bc957..9b2f428 100644 --- a/lib/views/login/login_controller.dart +++ b/lib/views/login/login_controller.dart @@ -4,12 +4,19 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; +import 'package:starwork_flutter/api/model/common/request/send_validation_code_request.dart'; +import 'package:starwork_flutter/api/service/common_api_service.dart'; import 'package:starwork_flutter/base/base_controller.dart'; -import 'package:starwork_flutter/common/enums/login_type.dart'; +import 'package:starwork_flutter/common/constant/login_type.dart'; +import 'package:starwork_flutter/common/constant/validation_code_channel.dart'; +import 'package:starwork_flutter/common/constant/validation_code_type.dart'; + import 'package:starwork_flutter/routes/app_pages.dart'; import 'package:starwork_flutter/routes/app_routes.dart'; class LoginController extends BaseController { + final commonApi = Get.find(); + int phoneNumberSize = 11; TextEditingController phoneController = TextEditingController(); TextEditingController passwordController = TextEditingController(); @@ -17,7 +24,7 @@ class LoginController extends BaseController { final isFormValid = false.obs; final isPasswordVisible = true.obs; final isPrivacyAgreementValid = 0.obs; - final loginType = LoginType.phoneCode.obs; + final loginType = LoginType.phoneCodeLogin.obs; @override void onInit() { @@ -41,28 +48,34 @@ class LoginController extends BaseController { } // 获取手机验证码 - void requestPhoneCode() { + void requestPhoneCode() async { if (isPrivacyAgreementValid.value != 1) { _showCustomDialog( title: '欢迎使用星勤'.tr, content: '1', - onConfirm: () { + onConfirm: () async { isPrivacyAgreementValid.value = 1; - Get.toNamed( - AppRoutes.inputVerificationCode, - parameters: { - 'phone': phoneController.text.trim(), - - }, - ); }, ); - } else { + } + _handleRequestPhoneCode(); + } + + void _handleRequestPhoneCode() async { + var sendValidationCodeResult = await commonApi.sendValidationCode( + request: SendValidationCodeRequest( + countryCode: '+86', + account: phoneController.text.trim(), + channel: ValidationCodeChannel.sms.value, + codeType: ValidationCodeType.login, + ), + ); + + if (sendValidationCodeResult.isSuccess) { Get.toNamed( AppRoutes.inputVerificationCode, parameters: { 'phone': phoneController.text.trim(), - }, ); } diff --git a/lib/views/login/login_view.dart b/lib/views/login/login_view.dart index 0b32301..2416512 100644 --- a/lib/views/login/login_view.dart +++ b/lib/views/login/login_view.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:starwork_flutter/common/enums/login_type.dart'; +import 'package:starwork_flutter/common/constant/login_type.dart'; + import 'package:starwork_flutter/routes/app_routes.dart'; import 'login_controller.dart'; @@ -173,7 +174,7 @@ class LoginView extends GetView { ), ), child: Text( - controller.loginType.value == LoginType.phoneCode + controller.loginType.value == LoginType.phoneCodeLogin ? '获取验证码'.tr : '登录'.tr, style: TextStyle( @@ -188,7 +189,7 @@ class LoginView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ Visibility( - visible: LoginType.phoneCode == controller.loginType.value, + visible: LoginType.phoneCodeLogin == controller.loginType.value, child: TextButton( onPressed: () { controller.loginType.value = LoginType.phonePassword; @@ -208,7 +209,7 @@ class LoginView extends GetView { visible: LoginType.phonePassword == controller.loginType.value, child: TextButton( onPressed: () { - controller.loginType.value = LoginType.phoneCode; + controller.loginType.value = LoginType.phoneCodeLogin; }, child: Text( '验证码登录'.tr, @@ -236,7 +237,8 @@ class LoginView extends GetView { visible: LoginType.phonePassword == controller.loginType.value, child: TextButton( style: ButtonStyle( - overlayColor: MaterialStateProperty.all(Colors.transparent), // 墨水覆盖 + overlayColor: + MaterialStateProperty.all(Colors.transparent), // 墨水覆盖 side: MaterialStateProperty.all(null), // 取消边框 ), onPressed: () { @@ -266,7 +268,7 @@ class LoginView extends GetView { } _handleLoginButtonText() { - if (controller.loginType.value == LoginType.phoneCode) { + if (controller.loginType.value == LoginType.phoneCodeLogin) { return; } else if (controller.loginType.value == LoginType.phonePassword) { return '验证码登录'.tr + ' | '.tr + '忘记密码'.tr;