diff --git a/ln_jq_app/assets/images/welcome.png b/ln_jq_app/assets/images/welcome.png new file mode 100644 index 0000000..3a2d95e Binary files /dev/null and b/ln_jq_app/assets/images/welcome.png differ diff --git a/ln_jq_app/lib/main.dart b/ln_jq_app/lib/main.dart index 1a5f1d1..99ae966 100644 --- a/ln_jq_app/lib/main.dart +++ b/ln_jq_app/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:get_storage/get_storage.dart'; @@ -8,8 +9,8 @@ import 'package:ln_jq_app/storage_service.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'common/styles/theme.dart'; -import 'pages/home/view.dart'; import 'pages/login/view.dart'; +import 'pages/welcome/view.dart'; // 引入启动页 void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -17,8 +18,10 @@ void main() async { WidgetsBinding widgetsBinding = await init( isDebug: true, logTag: '小羚羚', - supportedLocales: [Locale('zh', 'CN')], + supportedLocales: [const Locale('zh', 'CN')], ); + + // 保持原生闪屏页,直到 WelcomeController 调用 remove() FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await GetStorage.init(); @@ -28,36 +31,24 @@ void main() async { runApp( GetxApp( - // 设计稿尺寸 单位:dp designSize: const Size(390, 844), - // Getx Log enableLog: true, - // 默认的跳转动画 defaultTransition: Transition.rightToLeft, - // 主题模式 themeMode: GlobalService.to.themeMode, - // 主题 theme: AppTheme.light, - // Dark主题 darkTheme: AppTheme.light, - // AppTitle title: '小羚羚', - // 首页入口 - home: HomePage(), - //组件国际化 - fallbackLocale: Locale('zh', 'CN'), - supportedLocales: [Locale('zh', 'CN')], + // 将入口改为启动页 + home: const WelcomePage(), + fallbackLocale: const Locale('zh', 'CN'), + supportedLocales: const [Locale('zh', 'CN')], localizationsDelegates: const [ - //pull_to_refresh RefreshLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - - // Builder builder: (context, widget) { - // do something.... return widget!; }, ), @@ -67,20 +58,16 @@ void main() async { void initHttpSet() { AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url; - // 设置基础 URL HttpService.to.setBaseUrl(AppTheme.test_service_url); - //指定请求头 HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token')); - // 设置全局响应处理器 HttpService.to.setOnResponseHandler((response) async { try { final baseModel = BaseModel.fromJson(response.data); if (baseModel.code == 0 || baseModel.code == 200) { - return null; } else if (baseModel.code == 401) { await StorageService.to.clearLoginInfo(); - Get.offAll(() => LoginPage()); + Get.offAll(() => const LoginPage()); return baseModel.message; } else { return (baseModel.error.toString()).isEmpty diff --git a/ln_jq_app/lib/pages/login/controller.dart b/ln_jq_app/lib/pages/login/controller.dart index 8b617e4..0566464 100644 --- a/ln_jq_app/lib/pages/login/controller.dart +++ b/ln_jq_app/lib/pages/login/controller.dart @@ -13,11 +13,10 @@ class LoginController extends GetxController with BaseControllerMixin { 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; @@ -75,6 +74,9 @@ class LoginController extends GetxController with BaseControllerMixin { _timer?.cancel(); phoneController.dispose(); codeController.dispose(); + + stationIdController.dispose(); + passwordController.dispose(); super.onClose(); } } diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index f9d6790..3b09de4 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -24,119 +24,165 @@ class LoginPage extends StatefulWidget { State createState() => _LoginPageState(); } -class _LoginPageState extends State { +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: 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"), + body: GestureDetector( + onTap: () { + hideKeyboard(); + }, + child: Stack( + children: [ + // 1. 顶部背景与装饰 + Positioned( + top: 0, + left: 0, + right: 0, + child: LoginUtil.getAssImg("bg_login"), ), - ), - _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), - ), + Positioned( + top: 0, + left: 0, + child: SizedBox( + width: 180.w, + height: 218.h, + child: LoginUtil.getAssImg("ic_login_bg@2x"), ), - 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( + ), + _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: [ - 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, - ), - ), + 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(() => UrlHostPage()); - }, - child: const Text( - "域名配置", - style: TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, + + 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 _buildBackground() { + /// 品牌头部 + Widget _buildBrandingHeader() { return Positioned( top: 0, left: 32.w, @@ -145,7 +191,6 @@ class _LoginPageState extends State { 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( @@ -170,7 +215,7 @@ class _LoginPageState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( - color: Color.fromRGBO(56, 198, 151, 1), + color: const Color.fromRGBO(56, 198, 151, 1), borderRadius: BorderRadius.circular(20), ), child: const Text( @@ -189,50 +234,32 @@ class _LoginPageState extends State { ); } - /// 构建登录输入框区域 - Widget _buildLoginInputFields(LoginController controller) { + /// 司机登录输入框 (手机号+验证码) + Widget _buildDriverInputFields(LoginController controller) { return Column( children: [ - // 手机号输入 - Container( - height: 55.h, - decoration: BoxDecoration( - color: const Color(0xFFF7F9FB), - borderRadius: BorderRadius.circular(28), - ), + _buildInputWrapper( 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), - ), + _buildInputWrapper( child: Row( children: [ Expanded( child: TextField( - inputFormatters: [ - LengthLimitingTextInputFormatter(6), // 最多6位 - ], controller: controller.codeController, keyboardType: TextInputType.number, - style: const TextStyle(fontSize: 15), + inputFormatters: [LengthLimitingTextInputFormatter(6)], decoration: const InputDecoration( hintText: '请输入验证码', - hintStyle: TextStyle(color: Colors.grey, fontSize: 14), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 24), ), @@ -248,7 +275,7 @@ class _LoginPageState extends State { child: Text( controller.countdown.value == 0 ? "获取验证码" - : "${controller.countdown.value}s后重新获取", + : "${controller.countdown.value}s后重发", style: TextStyle( color: controller.countdown.value == 0 ? const Color(0xFF006633) @@ -263,27 +290,97 @@ class _LoginPageState extends State { ], ), ), - 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, + ], + ); + } + + /// 站点登录输入框 (账号+密码) + 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), + ), ), - child: const Text( - "立即登录", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + 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( @@ -298,10 +395,16 @@ class _LoginPageState extends State { ); 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; @@ -317,86 +420,110 @@ class _LoginPageState extends State { '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('登录失败:数据异常'); - } + _processLoginResponse(responseData, "driver", phone); } catch (e) { dismissLoading(); } } - bool _isAgreed = false; + /// 站点登录逻辑 (账号密码) + 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( @@ -478,17 +605,30 @@ class _LoginPageState extends State { Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url}); } - final _aliyunPush = AliyunPushFlutter(); - - void addAlias(String alias) async { - var result = await _aliyunPush.addAlias(alias); - var code = result['code']; - if (code == kAliyunPushSuccessCode) { - Logger.d('添加别名$alias成功'); - } else { - var errorCode = result['code']; - var errorMsg = result['errorMsg']; - Logger.d('添加别名$alias失败: $errorCode - $errorMsg'); - } + 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, + ), + ), + ], + ), + ); } } diff --git a/ln_jq_app/lib/pages/welcome/controller.dart b/ln_jq_app/lib/pages/welcome/controller.dart new file mode 100644 index 0000000..151e33a --- /dev/null +++ b/ln_jq_app/lib/pages/welcome/controller.dart @@ -0,0 +1,32 @@ +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:get/get.dart'; +import 'package:ln_jq_app/pages/home/view.dart'; +import 'package:ln_jq_app/pages/login/view.dart'; +import 'package:ln_jq_app/storage_service.dart'; + +class WelcomeController extends GetxController { + @override + void onReady() { + super.onReady(); + // 移除原生闪屏页(如果有的话) + FlutterNativeSplash.remove(); + _startTimer(); + } + + void _startTimer() { + // 1.5秒后执行跳转逻辑 + Future.delayed(const Duration(milliseconds: 1500), () { + Get.offAll(() => const HomePage()); + }); + } + + void _jumpToNextPage() { + if (StorageService.to.isLoggedIn) { + // 已登录,跳转到首页 + Get.offAll(() => const HomePage()); + } else { + // 未登录,跳转到登录页 + Get.offAll(() => const LoginPage()); + } + } +} diff --git a/ln_jq_app/lib/pages/welcome/view.dart b/ln_jq_app/lib/pages/welcome/view.dart new file mode 100644 index 0000000..3f67a90 --- /dev/null +++ b/ln_jq_app/lib/pages/welcome/view.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ln_jq_app/common/login_util.dart'; +import 'controller.dart'; + +class WelcomePage extends GetView { + const WelcomePage({super.key}); + + @override + Widget build(BuildContext context) { + // 初始化控制器 + Get.put(WelcomeController()); + + return Scaffold( + backgroundColor: Colors.white, + body: SizedBox.expand( + child: Stack( + children: [ + Positioned( + top: 0, + bottom: 0, + left: 0, + right: 0, + child: Image.asset( + 'assets/images/welcome.png', + fit: BoxFit.fill + ), + ), + ], + ), + ), + ); + } +}