diff --git a/ln_jq_app/assets/images/bg_login.png b/ln_jq_app/assets/images/bg_login.png new file mode 100644 index 0000000..2ebbed0 Binary files /dev/null and b/ln_jq_app/assets/images/bg_login.png differ diff --git a/ln_jq_app/assets/images/ic_login_bg@2x.png b/ln_jq_app/assets/images/ic_login_bg@2x.png new file mode 100644 index 0000000..815b394 Binary files /dev/null and b/ln_jq_app/assets/images/ic_login_bg@2x.png differ diff --git a/ln_jq_app/assets/images/ic_logo_unbg@2x.png b/ln_jq_app/assets/images/ic_logo_unbg@2x.png new file mode 100644 index 0000000..233bd9a Binary files /dev/null and b/ln_jq_app/assets/images/ic_logo_unbg@2x.png differ diff --git a/ln_jq_app/lib/pages/login/controller.dart b/ln_jq_app/lib/pages/login/controller.dart index aa5d726..8b617e4 100644 --- a/ln_jq_app/lib/pages/login/controller.dart +++ b/ln_jq_app/lib/pages/login/controller.dart @@ -1,4 +1,7 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:ln_jq_app/common/model/base_model.dart'; class LoginController extends GetxController with BaseControllerMixin { @override @@ -7,19 +10,71 @@ class LoginController extends GetxController with BaseControllerMixin { LoginController(); // 控制输入框的 TextEditingController + final TextEditingController phoneController = TextEditingController(); + final TextEditingController codeController = TextEditingController(); + + // 兼容旧逻辑 final TextEditingController driverIdentityController = TextEditingController(); final TextEditingController stationIdController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); + // --- 倒计时逻辑 --- + final RxInt countdown = 0.obs; + Timer? _timer; + + void startCountdown() async { + if (phoneController.text.isEmpty || !phoneController.text.isPhoneNumber) { + showToast("请输入正确的手机号"); + return; + } + + if (countdown.value > 0) return; + + // 调用发送验证码接口 + var responseData = await HttpService.to.post( + 'appointment/login/sendCode', + data: {"mobile": phoneController.text}, + ); + + if (responseData == null && responseData!.data == null) { + showToast('验证码发送失败,请稍后重试'); + return; + } + + try { + var result = BaseModel.fromJson(responseData.data); + + if (result.code != 0) { + showToast(result.error); + dismissLoading(); + return; + } + + showToast("验证码已发送"); + + countdown.value = 60; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (countdown.value > 0) { + countdown.value--; + } else { + _timer?.cancel(); + } + }); + } catch (e) { + showToast('验证码服务异常,请稍后重试'); + } + } + @override void onInit() { - super.onInit(); } @override void onClose() { + _timer?.cancel(); + phoneController.dispose(); + codeController.dispose(); super.onClose(); } - } diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index 93d5b54..f9d6790 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:aliyun_push_flutter/aliyun_push_flutter.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:geolocator/geolocator.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:ln_jq_app/common/login_util.dart'; @@ -24,411 +24,376 @@ class LoginPage extends StatefulWidget { State createState() => _LoginPageState(); } -class _LoginPageState extends State with SingleTickerProviderStateMixin { - late TabController tabController; - - bool cLogin = true; - bool _obscureText = true; - - // 用于管理“记住密码”的复选框状态 - bool _rememberPassword = true; - - // 用于确保凭证只在首次加载时回填一次 - bool _credentialsLoaded = false; - +class _LoginPageState extends State { @override - void initState() { - super.initState(); - tabController = TabController(length: 2, vsync: this); - tabController.addListener(_tabChangeListener); - } - - void _tabChangeListener() { - if (!tabController.indexIsChanging) { - switchTab(tabController.index); - } - } - - void switchTab(int index) { - setState(() { - cLogin = (index == 0); - }); - } - - @override - void dispose() { - tabController.dispose(); - super.dispose(); - } - - Widget _buildView(LoginController controller) { - // 在视图构建时,检查并回填已保存的凭证 - if (!_credentialsLoaded) { - final savedAccount = StorageService.to.stationAccount; - final savedPassword = StorageService.to.stationPassword; - if (savedAccount != null && savedPassword != null) { - controller.stationIdController.text = savedAccount; - controller.passwordController.text = savedPassword; - _rememberPassword = true; // 如果有保存的密码,则默认勾选 - } - _credentialsLoaded = true; // 标记为已加载,防止重复执行 - } - - 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 ? 295.h : 370.h, - padding: EdgeInsets.all(15), - child: Column( - children: [ - Card( - elevation: 2, - child: Container( - height: 55.h, - padding: EdgeInsets.all(3), - child: TabBar( - controller: tabController, - onTap: (index) { - delayed(300, () { - switchTab(index); - }); - }, - labelColor: Colors.white, - unselectedLabelColor: Colors.black, - indicator: BoxDecoration( - color: AppTheme.themeColor, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 6, + Widget build(BuildContext context) { + return GetBuilder( + init: LoginController(), + id: 'login', + builder: (controller) { + return Scaffold( + backgroundColor: Colors.white, + body: Stack( + children: [ + // 1. 顶部背景与装饰 + Positioned( + top: 0, + left: 0, + right: 0, + child: LoginUtil.getAssImg("bg_login"), + ), + Positioned( + top: 0, + left: 0, + child: SizedBox( + width: 180.w, + height: 218.h, + child: LoginUtil.getAssImg("ic_login_bg@2x"), + ), + ), + _buildBackground(), + // 2. 登录表单主体 + Positioned( + top: 280.h, + left: 0, + right: 0, + bottom: 0, + child: Container( + height: MediaQuery.of(context).size.height * 2 / 3, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(40), + topRight: Radius.circular(40), + ), + ), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 30.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 80.h), + // 登录输入区域 + _buildLoginInputFields(controller), + SizedBox(height: 40.h), + // 协议 + buildAgreement(), + SizedBox(height: 80.h), + // 底部 Slogan + Center( + child: Column( + children: [ + Text( + "H Y P A I", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Color.fromRGBO(51, 51, 51, 1), + letterSpacing: 8, + ), + ), + Text( + "HYDROGEN MOBILITY", + style: TextStyle( + fontSize: 9, + color: Colors.grey.shade400, + letterSpacing: 1, + ), + ), + ], + ), ), ], ), - tabs: [ - Tab(text: '司机端登录'), - Tab(text: '加氢站登录'), - ], - isScrollable: false, ), ), ), - Flexible( - child: TabBarView( - controller: tabController, - children: [ - _driverLoginView(controller), - _stationLoginView(controller), - ], + ), + if (AppTheme.is_show_host) + Positioned( + top: 40.h, + right: 20.w, + child: TextButton( + onPressed: () { + Get.to(() => UrlHostPage()); + }, + child: const Text( + "域名配置", + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), ), ), - ], - ), + ], ), - ), - ].toColumn(mainAxisSize: MainAxisSize.min).center(), + ); + }, ); } - Widget _driverLoginView(LoginController controller) { - return !cLogin - ? SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + /// 构建背景装饰 + Widget _buildBackground() { + return Positioned( + top: 0, + left: 32.w, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 100.h), + // Logo + SizedBox(height: 60.h, child: LoginUtil.getAssImg('ic_logo_unbg@2x')), + SizedBox(height: 30.h), + const Text( + "HELLO,", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w500, + color: Color.fromRGBO(51, 51, 51, 1), + ), + ), + const SizedBox(height: 8), + Row( children: [ - SizedBox(height: 20.h), - TextFormField( - controller: controller.driverIdentityController, - cursorColor: AppTheme.themeColor, - maxLength: 8, - style: TextStyle(fontSize: 14), - decoration: InputDecoration( - hintText: '请输入身份后8位', - border: OutlineInputBorder(), - hintStyle: TextStyle(fontSize: 14), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), - prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppTheme.themeColor), + const Text( + "欢迎使用 ", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Color.fromRGBO(51, 51, 51, 1), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Color.fromRGBO(56, 198, 151, 1), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + "“羚牛氢能智慧服务平台”", + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, ), ), ), - SizedBox(height: 20.h), - ElevatedButton( - onPressed: () async { - if (!_isAgreed) { - DialogX.to.showConfirmDialog( - icon: DialogIcon.warn, - content: _buildDialogContent(), - confirmText: '同意', - cancelText: '拒绝', - onConfirm: () { - _isAgreed = true; - controller.updateUi(); - }, - ); - return; - } - - String password = controller.driverIdentityController.text; - if (password.isEmpty) { - showToast("请输入密码"); - return; - } - showLoading('登录中...'); - try { - var responseData = await HttpService.to.post( - 'appointment/login/loginForDriver', - data: {'idNo': password}, - ); - if (responseData == null && responseData!.data == null) { - dismissLoading(); - showToast('登录失败:无法获取凭证'); - return; - } - //登录信息处理 - try { - var result = BaseModel.fromJson(responseData.data); - - if (result.code != 0) { - showToast(result.error); - dismissLoading(); - return; - } - - String token = result.data['token'] ?? ''; - String idCard = result.data['idCard'] ?? ''; - String name = result.data['name'] ?? ''; - String phone = result.data['phone'] ?? ''; - await StorageService.to.saveLoginInfo( - token: token, - userId: "", - channel: "driver", - idCard: idCard, - name: name, - phone: phone, - ); - - //注册推送别名 - addAlias(phone); - - //登录后查询已绑定车辆信息 - var carInfo = await HttpService.to.get( - "appointment/driver/getTruckInfoByDriver?phone=$phone", - ); - if (carInfo != null) { - var carInforesult = BaseModel.fromJson(carInfo.data); - if (carInforesult.data != null) { - final vehicle = VehicleInfo.fromJson( - carInforesult.data as Map, - ); - //保存使用 - await StorageService.to.saveVehicleInfo(vehicle); - } - } - - //页面操作 - dismissLoading(); - showToast('登录成功,欢迎您'); - Get.offAll(() => BaseWidgetsPage()); - } catch (e) { - dismissLoading(); - showToast('登录失败:数据异常'); - } - } catch (e) { - dismissLoading(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.themeColor, - minimumSize: Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: Text('登录'), - ), - buildAgreement(), ], - ); + ), + ], + ), + ); } - Widget _stationLoginView(LoginController controller) { - return cLogin - ? SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + /// 构建登录输入框区域 + Widget _buildLoginInputFields(LoginController controller) { + return Column( + children: [ + // 手机号输入 + Container( + height: 55.h, + decoration: BoxDecoration( + color: const Color(0xFFF7F9FB), + borderRadius: BorderRadius.circular(28), + ), + child: TextField( + controller: controller.phoneController, + keyboardType: TextInputType.phone, + style: const TextStyle(fontSize: 15), + decoration: const InputDecoration( + hintText: '请输入手机号', + hintStyle: TextStyle(color: Colors.grey, fontSize: 14), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 24), + ), + ), + ), + const SizedBox(height: 20), + // 验证码输入 + Container( + height: 55.h, + decoration: BoxDecoration( + color: const Color(0xFFF7F9FB), + borderRadius: BorderRadius.circular(28), + ), + child: Row( children: [ - SizedBox(height: 20), - TextFormField( - controller: controller.stationIdController, - cursorColor: AppTheme.themeColor, - style: TextStyle(fontSize: 14), - decoration: InputDecoration( - hintText: '请输入加氢站编号', - border: OutlineInputBorder(), - hintStyle: TextStyle(fontSize: 14), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), - prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppTheme.themeColor), + Expanded( + child: TextField( + inputFormatters: [ + LengthLimitingTextInputFormatter(6), // 最多6位 + ], + controller: controller.codeController, + keyboardType: TextInputType.number, + style: const TextStyle(fontSize: 15), + decoration: const InputDecoration( + hintText: '请输入验证码', + hintStyle: TextStyle(color: Colors.grey, fontSize: 14), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 24), ), ), ), - SizedBox(height: 10), - TextFormField( - controller: controller.passwordController, - obscureText: _obscureText, - style: TextStyle(fontSize: 14), - cursorColor: AppTheme.themeColor, - decoration: InputDecoration( - hintStyle: TextStyle(fontSize: 14), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), - hintText: '请输入密码', - border: OutlineInputBorder(), - suffixIcon: IconButton( - icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - prefixIcon: Icon(Icons.lock_outline, color: Colors.grey), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppTheme.themeColor), - ), - ), - ), - SizedBox(height: 10), - //记住密码复选框 --- - Row( - children: [ - SizedBox( - height: 24, - width: 24, - child: Checkbox( - value: _rememberPassword, - activeColor: AppTheme.themeColor, - onChanged: (bool? value) { - setState(() { - _rememberPassword = value ?? true; - }); - }, + Obx( + () => GestureDetector( + onTap: controller.countdown.value == 0 + ? controller.startCountdown + : null, + child: Padding( + padding: const EdgeInsets.only(right: 24.0), + child: Text( + controller.countdown.value == 0 + ? "获取验证码" + : "${controller.countdown.value}s后重新获取", + style: TextStyle( + color: controller.countdown.value == 0 + ? const Color(0xFF006633) + : Colors.grey, + fontSize: 13, + fontWeight: FontWeight.bold, + ), ), ), - GestureDetector( - onTap: () => setState(() => _rememberPassword = !_rememberPassword), - child: const Text('记住密码', style: TextStyle(color: Colors.grey)), - ), - ], - ), - SizedBox(height: 20), // 调整间距 - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.themeColor, - minimumSize: Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - onPressed: () async { - if (!_isAgreed) { - DialogX.to.showConfirmDialog( - icon: DialogIcon.warn, - content: _buildDialogContent(), - confirmText: '同意', - cancelText: '拒绝', - onConfirm: () { - _isAgreed = true; - controller.updateUi(); - }, - ); - return; - } - - String account = controller.stationIdController.text; - String password = controller.passwordController.text; - - if (account.isEmpty || password.isEmpty) { - showToast("请输入账号和密码"); - return; - } - - showLoading('登录中...'); - - try { - String encryptedPassword = LoginUtil.encrypt(password); - var responseData = await HttpService.to.post( - 'appointment/login/password', - data: { - 'account': account, - 'password': encryptedPassword, - 'loginType': "station", - }, - ); - - if (responseData == null && responseData!.data == null) { - dismissLoading(); - showToast('登录失败:无法获取凭证'); - return; - } - - try { - var result = BaseModel.fromJson(responseData.data); - - if (result.code != 0) { - showToast(result.error); - dismissLoading(); - return; - } - - String token = result.data['token'] ?? ''; - String userId = result.data['userId'] ?? ''; - String mobile = result.data['mobile'] ?? ''; - - await StorageService.to.saveLoginInfo( - token: token, - userId: userId, - phone: mobile, - channel: "station", - ); - - //注册推送别名 - addAlias(mobile); - - // 根据复选框状态保存或清除密码 --- - if (_rememberPassword) { - await StorageService.to.saveStationCredentials(account, password); - } else { - await StorageService.to.clearStationCredentials(); - } - - dismissLoading(); - showToast('登录成功,欢迎您'); - Get.offAll(() => B_BaseWidgetsPage()); - } catch (e) { - dismissLoading(); - showToast('登录失败:数据异常'); - } - } catch (e) { - dismissLoading(); - } - }, - child: Text('登录'), ), - buildAgreement(), ], + ), + ), + const SizedBox(height: 40), + // 登录按钮 + ElevatedButton( + onPressed: () => _handleLogin(controller), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF006633), + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 55), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + elevation: 0, + ), + child: const Text( + "立即登录", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ], + ); + } + + /// 处理登录逻辑 + void _handleLogin(LoginController controller) async { + if (!_isAgreed) { + DialogX.to.showConfirmDialog( + icon: DialogIcon.warn, + content: _buildDialogContent(), + confirmText: '同意', + cancelText: '拒绝', + onConfirm: () { + _isAgreed = true; + controller.updateUi(); + }, + ); + return; + } + + String phone = controller.phoneController.text; + String code = controller.codeController.text; + + if (phone.isEmpty || !phone.isPhoneNumber) { + showToast("请输入正确的手机号"); + return; + } + if (code.isEmpty) { + showToast("请输入验证码"); + return; + } + + showLoading('登录中...'); + try { + var responseData = await HttpService.to.post( + 'appointment/login/login', + data: {'mobile': phone, 'code': code}, + ); + if (responseData == null && responseData!.data == null) { + dismissLoading(); + showToast('登录失败:无法获取凭证'); + return; + } + //登录信息处理 + try { + var result = BaseModel.fromJson(responseData.data); + + if (result.code != 0) { + showToast(result.error); + dismissLoading(); + return; + } + //类型2是司机 1是站点 + String loginType = result.data['loginType'].toString() ?? ''; + String token = result.data['token'] ?? ''; + + if (loginType == "2") { + String idCard = result.data['idCard'] ?? ''; + String name = result.data['name'] ?? ''; + String phone = result.data['phone'] ?? ''; + await StorageService.to.saveLoginInfo( + token: token, + userId: "", + channel: "driver", + idCard: idCard, + name: name, + phone: phone, ); + + //司机登录后查询已绑定车辆信息 + var carInfo = await HttpService.to.get( + "appointment/driver/getTruckInfoByDriver?phone=$phone", + ); + if (carInfo != null) { + var carInforesult = BaseModel.fromJson(carInfo.data); + if (carInforesult.data != null) { + final vehicle = VehicleInfo.fromJson( + carInforesult.data as Map, + ); + //保存使用 + await StorageService.to.saveVehicleInfo(vehicle); + } + } + } + + if (loginType == "1") { + String userId = result.data['userId'] ?? ''; + String mobile = result.data['mobile'] ?? ''; + await StorageService.to.saveLoginInfo( + token: token, + userId: userId, + phone: mobile, + channel: "station", + ); + } + + //注册推送别名 + addAlias(phone); + + //页面操作 + dismissLoading(); + showToast('登录成功,欢迎您'); + + if (loginType == "2") { + Get.offAll(() => BaseWidgetsPage()); + } else { + Get.offAll(() => B_BaseWidgetsPage()); + } + } catch (e) { + dismissLoading(); + showToast('登录失败:数据异常'); + } + } catch (e) { + dismissLoading(); + } } bool _isAgreed = false; @@ -526,39 +491,4 @@ class _LoginPageState extends State with SingleTickerProviderStateMix Logger.d('添加别名$alias失败: $errorCode - $errorMsg'); } } - - @override - Widget build(BuildContext context) { - return GetBuilder( - init: LoginController(), - id: 'login', - builder: (controller) { - return Scaffold( - body: Stack( - children: [ - Positioned.fill(child: _buildView(controller)), - if (AppTheme.is_show_host) - Positioned( - top: 40.h, - right: 20.w, - child: TextButton( - onPressed: () { - Get.to(() => const UrlHostPage()); - }, - child: const Text( - "域名配置", - style: TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } }