diff --git a/ln_jq_app/lib/common/login_util.dart b/ln_jq_app/lib/common/login_util.dart new file mode 100644 index 0000000..f435d33 --- /dev/null +++ b/ln_jq_app/lib/common/login_util.dart @@ -0,0 +1,37 @@ +import 'package:encrypt/encrypt.dart'; + +class LoginUtil { + // 定义密钥 + // 对于ECB模式,我们只需要Key,不需要IV (初始化向量) + static final _keyString = '915eae87951a448c86c47796e44c1fcf'; + static final _key = Key.fromUtf8(_keyString); + + // 【核心修改】创建使用 ECB 模式的加密器实例 + // 1. mode: AESMode.ecb -> 使用ECB模式,它不需要IV。 + // 2. padding: 'PKCS7' -> 在encrypt库中,PKCS5和PKCS7的填充方式是兼容的。 + static final _encrypter = Encrypter(AES(_key, mode: AESMode.ecb, padding: 'PKCS7')); + + /// AES 加密方法 + static String encrypt(String plainText) { + if (plainText.isEmpty) { + return ''; + } + // 【核心修改】调用 encrypt 方法时不再需要传递 iv + final encrypted = _encrypter.encrypt(plainText); + + // 返回Base64编码的密文,这是网络传输的标准做法 + return encrypted.base64; + } + + /// AES 解密方法 (可选,如果需要解密的话) + static String decrypt(String encryptedText) { + if (encryptedText.isEmpty) { + return ''; + } + final encrypted = Encrypted.fromBase64(encryptedText); + // 【核心修改】调用 decrypt 方法时不再需要传递 iv + final decrypted = _encrypter.decrypt(encrypted); + return decrypted; + } +} + diff --git a/ln_jq_app/lib/common/model/base_model.dart b/ln_jq_app/lib/common/model/base_model.dart index c4d2492..a163834 100644 --- a/ln_jq_app/lib/common/model/base_model.dart +++ b/ln_jq_app/lib/common/model/base_model.dart @@ -1,19 +1,75 @@ -class BaseModel { - bool? success; - String? type; - String? url; +/// 通用的 API 响应基础模型 +/// 使用泛型 来适应 'data' 字段中任何可能的数据类型 +class BaseModel { + final int code; // 状态码,0正常,其他异常 + final bool status; // 状态布尔值,true正常,false异常 + final String message; // 消息,例如 "success" + final T? data; // 核心数据,使用泛型 T,可以是任何类型 + final int time; // 时间戳 + final dynamic error; // 错误信息,可以是任何类型或 null - BaseModel({this.success, this.type, this.url}); + BaseModel({ + required this.code, + required this.status, + required this.message, + this.data, // data 可以为 null + required this.time, + this.error, // error 可以为 null + }); - factory BaseModel.fromJson(Map json) => BaseModel( - success: json['success']?.toString().contains("true"), - type: json['type']?.toString(), - url: json['url']?.toString(), - ); + /// fromJson 工厂构造函数(重构后) + factory BaseModel.fromJson( + Map json, { + T? Function(dynamic dataJson)? dataBuilder, + }) { + // 使用一个辅助函数来安全地转换类型,防止因类型不匹配(如 "0" vs 0)而崩溃 + int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? -1; + return -1; + } + T? finalData; + + // 检查 'data' 字段是否存在 + if (json.containsKey('data') && json['data'] != null) { + if (dataBuilder != null) { + // 如果提供了 dataBuilder,就用它来解析成具体的 T 类型对象 + finalData = dataBuilder(json['data']); + } else { + // 如果没有提供 dataBuilder,但 T 不是 dynamic,我们假设 data 就是 T 类型 + // 这在使用 BaseModel> 时很有用 + if (T != dynamic) { + try { + finalData = json['data'] as T?; + } catch(e) { + // 如果直接转换失败,保持为 null,避免崩溃 + finalData = null; + } + } else { + // 如果 T 是 dynamic,直接赋值 + finalData = json['data']; + } + } + } + + return BaseModel( + code: _parseInt(json['code']), + status: json['status'] as bool? ?? false, + message: json['message']?.toString() ?? 'No message', + time: _parseInt(json['time']), + data: finalData, + error: json['error'], + ); + } + + /// toJson 方法 (可选) Map toJson() => { - if (success != null) 'success': success, - if (type != null) 'type': type, - if (url != null) 'url': url, - }; + 'code': code, + 'status': status, + 'message': message, + 'time': time, + 'data': data, + 'error': error, + }; } diff --git a/ln_jq_app/lib/common/styles/theme.dart b/ln_jq_app/lib/common/styles/theme.dart index 1b7c85e..97f6426 100644 --- a/ln_jq_app/lib/common/styles/theme.dart +++ b/ln_jq_app/lib/common/styles/theme.dart @@ -7,6 +7,10 @@ class AppTheme { static const Color themeColor = Color(0xFF0c83c3); + + static const String test_service_url = "http://beta-esg.api.lnh2e.com/"; + static const String release_service_url = ""; + static const Color secondaryColor = Colors.orange; static const Color darkThemeColor = Color(0xFF032896); diff --git a/ln_jq_app/lib/main.dart b/ln_jq_app/lib/main.dart index 7966bc9..e2c0cee 100644 --- a/ln_jq_app/lib/main.dart +++ b/ln_jq_app/lib/main.dart @@ -1,23 +1,26 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:getx_scaffold/common/common.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; -import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart'; -import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; -import 'package:ln_jq_app/pages/home/view.dart'; +import 'package:ln_jq_app/common/model/base_model.dart'; +import 'package:ln_jq_app/storage_service.dart'; import 'common/styles/theme.dart'; -import 'pages/login/view.dart'; +import 'pages/home/view.dart'; -/// Main void main() async { - WidgetsBinding widgetsBinding = await init(isDebug: kDebugMode, logTag: '小羚羚'); + WidgetsBinding widgetsBinding = await init(isDebug: true, logTag: '小羚羚'); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + + await Get.putAsync(() => StorageService().init()); + initHttpSet(); + runApp( GetxApp( // 设计稿尺寸 单位:dp designSize: const Size(390, 844), // Getx Log - enableLog: kDebugMode, + enableLog: true, // 默认的跳转动画 defaultTransition: Transition.rightToLeft, // 主题模式 @@ -30,6 +33,10 @@ void main() async { title: '小羚羚', // 首页入口 home: HomePage(), + // 推荐使用命名路由,如果配置好了可以取消下面两行的注释 + // initialRoute: AppPages.INITIAL, + // getPages: AppPages.routes, + // Builder builder: (context, widget) { // do something.... @@ -39,4 +46,23 @@ void main() async { ); } +void initHttpSet() { + // 设置基础 URL + HttpService.to.setBaseUrl(AppTheme.test_service_url); + // 设置全局响应处理器 + HttpService.to.setOnResponseHandler((response) async { + try { + final baseModel = BaseModel.fromJson(response.data); + if (baseModel.code == 0) { + return null; + } else { + return baseModel.message; + } + } on Exception catch (e) { + e.printInfo(); + return '服务器异常'; + } + }); + +} diff --git a/ln_jq_app/lib/pages/b_page/reservation/controller.dart b/ln_jq_app/lib/pages/b_page/reservation/controller.dart index 1b0f620..f73b99d 100644 --- a/ln_jq_app/lib/pages/b_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/b_page/reservation/controller.dart @@ -2,6 +2,8 @@ import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:ln_jq_app/pages/login/view.dart'; +import '../../../storage_service.dart'; + class ReservationController extends GetxController with BaseControllerMixin { @override String get builderId => 'b_reservation'; // 确保ID与View中一致 @@ -50,11 +52,12 @@ class ReservationController extends GetxController with BaseControllerMixin { Get.snackbar('提示', '保存成功!'); // 示例:显示一个成功的提示 } - void logout() { + void logout() async{ // TODO: 在这里执行退出登录的逻辑 - // 1. 清理本地缓存的用户信息 - // 2. 调用退出登录接口 - // 3. 导航到登录页面 + //清理本地缓存的用户信息 导航到登录页面 + HttpService.to.clearAuthorization(); + await StorageService.to.clearLoginInfo(); + Get.offAll(() => LoginPage()); } } diff --git a/ln_jq_app/lib/pages/home/controller.dart b/ln_jq_app/lib/pages/home/controller.dart index 22d56c9..06dc783 100644 --- a/ln_jq_app/lib/pages/home/controller.dart +++ b/ln_jq_app/lib/pages/home/controller.dart @@ -4,6 +4,8 @@ import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/login/view.dart'; +import '../../storage_service.dart'; + class HomeController extends GetxController with BaseControllerMixin { @override String get builderId => 'home'; @@ -32,7 +34,7 @@ class HomeController extends GetxController with BaseControllerMixin { if (loginChannel == "b_login") { return BaseWidgetsPage(); // 渠道A进入 BaseWidgetsPage } else { - return B_BaseWidgetsPage(); // 渠道B进入 B_BaseWidgetsPage + return StorageService.to.isLoggedIn ? B_BaseWidgetsPage() : LoginPage(); // 渠道B进入 B_BaseWidgetsPage } } } diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index 9c28c1e..320df47 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:ln_jq_app/common/login_util.dart'; +import 'package:ln_jq_app/common/model/base_model.dart'; import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/login/controller.dart'; +import 'package:ln_jq_app/storage_service.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -54,76 +57,77 @@ class _LoginPageState extends State with SingleTickerProviderStateMix return Container( color: Color(0xFFEFF4F7), child: [ - Icon(cLogin ? AntdIcon.car : AntdIcon.USB), - SizedBox(height: 5.h), - TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700), - SizedBox(height: 5.h), - TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), // 设置圆角弧度 - ), - margin: EdgeInsets.all(15), - elevation: 4, - child: Container( - height: cLogin ? 260.h : 320.h, - padding: EdgeInsets.all(15), - child: // TabBar切换 - Column( - children: [ - Card( - elevation: 2, - child: Padding( - padding: EdgeInsets.all(3), - child: TabBar( - controller: tabController, - onTap: (index) { - //保证尺寸变化 - delayed(300, () { - switchTab(index); - }); - }, - // 修改TabBar的选中状态和未选中状态样式 - labelColor: Colors.white, - // 选中时的文字颜色 - unselectedLabelColor: Colors.black, - // 未选中时的文字颜色 - indicator: BoxDecoration( - color: AppTheme.themeColor, // 选中的Tab背景色(模拟卡片式效果) - borderRadius: BorderRadius.circular(12), // 卡片的圆角效果 - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 6, - ), + Icon(cLogin ? AntdIcon.car : AntdIcon.USB), + SizedBox(height: 5.h), + TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700), + SizedBox(height: 5.h), + TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"), + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), // 设置圆角弧度 + ), + margin: EdgeInsets.all(15), + elevation: 4, + child: Container( + height: cLogin ? 260.h : 320.h, + padding: EdgeInsets.all(15), + child: // TabBar切换 + Column( + children: [ + Card( + elevation: 2, + child: Padding( + padding: EdgeInsets.all(3), + child: TabBar( + controller: tabController, + onTap: (index) { + //保证尺寸变化 + delayed(300, () { + switchTab(index); + }); + }, + // 修改TabBar的选中状态和未选中状态样式 + labelColor: Colors.white, + // 选中时的文字颜色 + unselectedLabelColor: Colors.black, + // 未选中时的文字颜色 + indicator: BoxDecoration( + color: AppTheme.themeColor, // 选中的Tab背景色(模拟卡片式效果) + borderRadius: BorderRadius.circular(12), // 卡片的圆角效果 + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 6, + ), + ], + ), + tabs: [ + Tab(text: '司机端登录'), + Tab(text: '加氢站登录'), ], + isScrollable: false, ), - tabs: [ - Tab(text: '司机端登录'), - Tab(text: '加氢站登录'), - ], - isScrollable: false, ), ), - ), - // 根据选择的Tab展示不同的输入框 - Flexible( - child: TabBarView( - controller: tabController, - children: [ - // 司机端登录 - _driverLoginView(controller), - // 加氢站登录 - _stationLoginView(controller), - ], + // 根据选择的Tab展示不同的输入框 + Flexible( + child: TabBarView( + controller: tabController, + children: [ + // 司机端登录 + _driverLoginView(controller), + // 加氢站登录 + _stationLoginView(controller), + ], + ), ), - ), - ], + ], + ), ), ), - ), - ].toColumn(mainAxisSize: MainAxisSize.min).center(),); + ].toColumn(mainAxisSize: MainAxisSize.min).center(), + ); } // 司机端登录界面 @@ -151,14 +155,12 @@ class _LoginPageState extends State with SingleTickerProviderStateMix ElevatedButton( onPressed: () { // 司机端登录 - Get.offAll(() => BaseWidgetsPage()); + Get.offAll(() => BaseWidgetsPage()); }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.themeColor, minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度 - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: Text('登录'), ), @@ -215,13 +217,60 @@ class _LoginPageState extends State with SingleTickerProviderStateMix style: ElevatedButton.styleFrom( backgroundColor: AppTheme.themeColor, minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度 - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - onPressed: () { + onPressed: () async { // 加氢站登录逻辑 - Get.offAll(() => B_BaseWidgetsPage()); + String account = controller.stationIdController.text; + String password = controller.passwordController.text; + + //todo 删除 + account = "000017"; + password = "LnQn.314000"; + + if (account.isEmpty || password.isEmpty) { + showToast("请输入账号和密码"); + return; + } + + showLoading('登录中...'); + + try { + // 对密码进行AES加密 + String encryptedPassword = LoginUtil.encrypt(password); + + // 调用登录接口 + var responseData = await HttpService.to.post( + '/login/password', + data: { + 'account': account, + 'password': encryptedPassword, + 'loginType': "station", + }, + ); + + if (responseData == null && responseData!.data == null) { + dismissLoading(); + showToast('登录失败:无法获取凭证'); + return; + } + + final responseMap = responseData.data as Map; + + //保存用户信息 + String token = responseMap['token'] ?? ''; + //hydrogenId + String userId = responseMap['userId'] ?? ''; + await StorageService.to.saveLoginInfo(token: token, userId: userId); + + dismissLoading(); + showToast('登录成功,欢迎您'); + HttpService.to.setAuthorization(token); + // 跳转到主页,并清除所有历史页面 + Get.offAll(() => B_BaseWidgetsPage()); + } catch (e) { + dismissLoading(); + } }, child: Text('登录'), ), diff --git a/ln_jq_app/lib/storage_service.dart b/ln_jq_app/lib/storage_service.dart new file mode 100644 index 0000000..0be57ba --- /dev/null +++ b/ln_jq_app/lib/storage_service.dart @@ -0,0 +1,43 @@ +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +class StorageService extends GetxService { + late final GetStorage _box; + + // 定义存储时使用的键名(Key) + static const String _tokenKey = 'user_token'; + static const String _userIdKey = 'user_id'; + + // 提供一个静态的 'to' 方法,方便全局访问 + static StorageService get to => Get.find(); + + // Service 初始化 + Future init() async { + _box = GetStorage(); + return this; + } + + /// 判断是否已登录 (通过检查 token 是否存在且不为空) + bool get isLoggedIn => + _box + .read(_tokenKey) + ?.isNotEmpty ?? false; + + /// 获取 Token + String? get token => _box.read(_tokenKey); + + /// 获取 UserId + String? get userId => _box.read(_userIdKey); + + /// 保存用户信息 + Future saveLoginInfo({required String token, required String userId}) async { + await _box.write(_tokenKey, token); + await _box.write(_userIdKey, userId); + } + + /// 3. 删除用户信息 (用于退出登录) + Future clearLoginInfo() async { + await _box.remove(_tokenKey); + await _box.remove(_userIdKey); + } +} diff --git a/ln_jq_app/pubspec.lock b/ln_jq_app/pubspec.lock index 68d0963..b579876 100644 --- a/ln_jq_app/pubspec.lock +++ b/ln_jq_app/pubspec.lock @@ -178,7 +178,7 @@ packages: source: hosted version: "2.1.1" encrypt: - dependency: transitive + dependency: "direct main" description: name: encrypt sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" @@ -304,6 +304,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.7.2" + get_storage: + dependency: "direct main" + description: + name: get_storage + sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" getx_scaffold: dependency: "direct main" description: diff --git a/ln_jq_app/pubspec.yaml b/ln_jq_app/pubspec.yaml index 150001f..55ff4c6 100644 --- a/ln_jq_app/pubspec.yaml +++ b/ln_jq_app/pubspec.yaml @@ -35,6 +35,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 getx_scaffold: ^0.2.2 + encrypt: ^5.0.3 + get_storage: ^2.1.1 flutter_native_splash: ^2.4.7