import 'dart:io'; import 'package:aliyun_push_flutter/aliyun_push_flutter.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.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'; import 'package:ln_jq_app/common/model/base_model.dart'; import 'package:ln_jq_app/common/model/vehicle_info.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/common/webview/view.dart'; import 'package:ln_jq_app/pages/login/controller.dart'; import 'package:ln_jq_app/pages/url_host/view.dart'; import 'package:ln_jq_app/storage_service.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; bool _isAgreed = false; bool _obscureText = true; bool _rememberPassword = true; bool _credentialsLoaded = false; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _tabController.addListener(() { if (!_tabController.indexIsChanging) { setState(() {}); } }); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GetBuilder( init: LoginController(), id: 'login', builder: (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 Scaffold( backgroundColor: Colors.white, body: GestureDetector( onTap: () { hideKeyboard(); }, child: 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"), ), ), _buildBrandingHeader(), // 2. 登录表单主体 Positioned( top: 280.h, left: 0, right: 0, bottom: 0, child: Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(40), topRight: Radius.circular(40), ), ), child: Column( children: [ const SizedBox(height: 20), // TabBar 切换 Container( margin: EdgeInsets.symmetric(horizontal: 60.w), child: TabBar( controller: _tabController, indicatorColor: const Color(0xFF006633), indicatorWeight: 3, labelColor: const Color(0xFF006633), unselectedLabelColor: Colors.grey, labelStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), tabs: const [ Tab(text: "司机登录"), Tab(text: "站点登录"), ], ), ), Expanded( child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 30.w), child: Column( children: [ const SizedBox(height: 30), // 根据 Tab 显示不同的输入框 _tabController.index == 0 ? _buildDriverInputFields(controller) : _buildStationInputFields(controller), const SizedBox(height: 30), // 统一登录按钮 _buildLoginButton(controller), const SizedBox(height: 10), buildAgreement(), const SizedBox(height: 40), _buildFooterSlogan(), const SizedBox(height: 20), ], ), ), ), ], ), ), ), 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, ), ), ), ), ], ), ), ); }, ); } /// 品牌头部 Widget _buildBrandingHeader() { return Positioned( top: 0, left: 32.w, right: 0, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 100.h), 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: [ 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: const Color.fromRGBO(56, 198, 151, 1), borderRadius: BorderRadius.circular(20), ), child: const Text( "“羚牛氢能智慧服务平台”", style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), ], ), ); } /// 司机登录输入框 (手机号+验证码) Widget _buildDriverInputFields(LoginController controller) { return Column( children: [ _buildInputWrapper( child: TextField( controller: controller.phoneController, keyboardType: TextInputType.phone, decoration: const InputDecoration( hintText: '请输入手机号', border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 24), ), ), ), const SizedBox(height: 20), _buildInputWrapper( child: Row( children: [ Expanded( child: TextField( controller: controller.codeController, keyboardType: TextInputType.number, inputFormatters: [LengthLimitingTextInputFormatter(6)], decoration: const InputDecoration( hintText: '请输入验证码', border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 24), ), ), ), 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, ), ), ), ), ), ], ), ), ], ); } /// 站点登录输入框 (账号+密码) Widget _buildStationInputFields(LoginController controller) { return Column( children: [ _buildInputWrapper( child: TextField( controller: controller.stationIdController, decoration: const InputDecoration( hintText: '请输入加氢站编号', border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 24), ), ), ), const SizedBox(height: 20), _buildInputWrapper( child: Row( children: [ Expanded( child: TextField( controller: controller.passwordController, obscureText: _obscureText, decoration: const InputDecoration( hintText: '请输入密码', border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 24), ), ), ), IconButton( icon: Icon( _obscureText ? Icons.visibility_off : Icons.visibility, color: Colors.grey, ), onPressed: () => setState(() => _obscureText = !_obscureText), ), ], ), ), const SizedBox(height: 10), Row( children: [ SizedBox( width: 40, child: Checkbox( value: _rememberPassword, activeColor: const Color(0xFF006633), onChanged: (val) => setState(() => _rememberPassword = val ?? false), ), ), const Text("记住密码", style: TextStyle(color: Colors.grey, fontSize: 14)), ], ), ], ); } /// 通用输入框包装 Widget _buildInputWrapper({required Widget child}) { return Container( height: 55.h, decoration: BoxDecoration( color: const Color(0xFFF7F9FB), borderRadius: BorderRadius.circular(28), ), child: child, ); } /// 统一登录按钮逻辑 Widget _buildLoginButton(LoginController controller) { return 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: Text( _tabController.index == 0 ? "司机登录" : "站点登录", style: const 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; } _tabController.index == 0 ? _handleDriverLogin(controller) : _handleStationLogin(controller); } /// 司机登录逻辑 (短信登录) void _handleDriverLogin(LoginController controller) async { if (!_validateAgreed()) 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}, ); _processLoginResponse(responseData, "driver", phone); } catch (e) { dismissLoading(); } } /// 站点登录逻辑 (账号密码) void _handleStationLogin(LoginController controller) async { if (!_validateAgreed()) 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 (_rememberPassword) { await StorageService.to.saveStationCredentials(account, password); } else { await StorageService.to.clearStationCredentials(); } _processLoginResponse(responseData, "station", account); } catch (e) { dismissLoading(); } } bool _validateAgreed() { if (!_isAgreed) { DialogX.to.showConfirmDialog( icon: DialogIcon.warn, message: '请阅读并同意用户协议和隐私政策', confirmText: '确定', onConfirm: () {}, ); return false; } return true; } void _processLoginResponse( dynamic responseData, String channel, String identifier, ) async { if (responseData == null || responseData.data == null) { dismissLoading(); showToast('登录失败'); return; } var result = BaseModel.fromJson(responseData.data); if (result.code != 0) { showToast(result.error); dismissLoading(); return; } String token = result.data['token'] ?? ''; if (channel == "driver") { await StorageService.to.saveLoginInfo( token: token, userId: "", channel: "driver", name: result.data['name'], phone: result.data['phone'], ); // 成功后自动获取车辆信息 var carInfo = await HttpService.to.get( "appointment/driver/getTruckInfoByDriver?phone=${result.data['phone']}", ); if (carInfo != null) { var carResult = BaseModel.fromJson(carInfo.data); if (carResult.data != null) await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(carResult.data)); } dismissLoading(); Get.offAll(() => BaseWidgetsPage()); } else { await StorageService.to.saveLoginInfo( token: token, userId: result.data['userId'], phone: result.data['mobile'], channel: "station", ); dismissLoading(); Get.offAll(() => B_BaseWidgetsPage()); } addAlias(identifier); } final _aliyunPush = AliyunPushFlutter(); void addAlias(String alias) async { await _aliyunPush.addAlias(alias); } Widget buildAgreement() { return Padding( padding: EdgeInsets.only(top: 13.h), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 勾选框 SizedBox( width: 22.w, height: 22.h, child: Checkbox( value: _isAgreed, activeColor: AppTheme.themeColor, // 简单的圆角样式 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), onChanged: (bool? value) { setState(() { _isAgreed = value ?? false; }); }, ), ), const SizedBox(width: 4), // 富文本协议部分 Text.rich( TextSpan( text: '我已阅读并同意', style: const TextStyle(color: Colors.grey, fontSize: 13), children: [ TextSpan( text: '《用户协议》', style: TextStyle(color: AppTheme.themeColor, fontSize: 13), recognizer: TapGestureRecognizer() ..onTap = () { openPage("用户协议", "https://lnh2e.com/user_agreement.html"); }, ), const TextSpan(text: ' 和 '), TextSpan( text: '《隐私政策》', style: TextStyle(color: AppTheme.themeColor, fontSize: 13), recognizer: TapGestureRecognizer() ..onTap = () { openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"); }, ), ], ), ), ], ), ); } Widget _buildDialogContent() { return RichTextX( children: [ TextSpanItem('请阅读并同意'), TextSpanItem( '《隐私协议》', onTap: () => openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"), ), TextSpanItem('和'), TextSpanItem( '《用户政策》', onTap: () => openPage("用户协议", "https://lnh2e.com/user_agreement.html"), ), TextSpanItem(',我们将在协议框架内为您提供更优质的服务。'), ], ); } void openPage(String title, String url) { if (Platform.isIOS) { openWebPage(url); return; } Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url}); } Widget _buildFooterSlogan() { return Center( child: Column( children: [ Text( "H Y P A I", style: TextStyle( fontSize: 16, fontWeight: FontWeight.w400, color: const Color.fromRGBO(51, 51, 51, 1), letterSpacing: 8, ), ), Text( "HYDROGEN MOBILITY", style: TextStyle( fontWeight: FontWeight.w300, fontSize: 9, color: Colors.grey.shade400, letterSpacing: 1, ), ), ], ), ); } }