登录修改

This commit is contained in:
2026-01-26 14:07:58 +08:00
parent 9fdca9136d
commit 907983a1d1
5 changed files with 398 additions and 413 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
class LoginController extends GetxController with BaseControllerMixin { class LoginController extends GetxController with BaseControllerMixin {
@override @override
@@ -7,19 +10,71 @@ class LoginController extends GetxController with BaseControllerMixin {
LoginController(); LoginController();
// 控制输入框的 TextEditingController // 控制输入框的 TextEditingController
final TextEditingController phoneController = TextEditingController();
final TextEditingController codeController = TextEditingController();
// 兼容旧逻辑
final TextEditingController driverIdentityController = TextEditingController(); final TextEditingController driverIdentityController = TextEditingController();
final TextEditingController stationIdController = TextEditingController(); final TextEditingController stationIdController = TextEditingController();
final TextEditingController passwordController = 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 @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
} }
@override @override
void onClose() { void onClose() {
_timer?.cancel();
phoneController.dispose();
codeController.dispose();
super.onClose(); super.onClose();
} }
} }

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart'; import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/common/login_util.dart';
@@ -24,411 +24,376 @@ class LoginPage extends StatefulWidget {
State<LoginPage> createState() => _LoginPageState(); State<LoginPage> createState() => _LoginPageState();
} }
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin { class _LoginPageState extends State<LoginPage> {
late TabController tabController;
bool cLogin = true;
bool _obscureText = true;
// 用于管理“记住密码”的复选框状态
bool _rememberPassword = true;
// 用于确保凭证只在首次加载时回填一次
bool _credentialsLoaded = false;
@override @override
void initState() { Widget build(BuildContext context) {
super.initState(); return GetBuilder<LoginController>(
tabController = TabController(length: 2, vsync: this); init: LoginController(),
tabController.addListener(_tabChangeListener); id: 'login',
} builder: (controller) {
return Scaffold(
void _tabChangeListener() { backgroundColor: Colors.white,
if (!tabController.indexIsChanging) { body: Stack(
switchTab(tabController.index); children: [
} // 1. 顶部背景与装饰
} Positioned(
top: 0,
void switchTab(int index) { left: 0,
setState(() { right: 0,
cLogin = (index == 0); child: LoginUtil.getAssImg("bg_login"),
}); ),
} Positioned(
top: 0,
@override left: 0,
void dispose() { child: SizedBox(
tabController.dispose(); width: 180.w,
super.dispose(); height: 218.h,
} child: LoginUtil.getAssImg("ic_login_bg@2x"),
),
Widget _buildView(LoginController controller) { ),
// 在视图构建时,检查并回填已保存的凭证 _buildBackground(),
if (!_credentialsLoaded) { // 2. 登录表单主体
final savedAccount = StorageService.to.stationAccount; Positioned(
final savedPassword = StorageService.to.stationPassword; top: 280.h,
if (savedAccount != null && savedPassword != null) { left: 0,
controller.stationIdController.text = savedAccount; right: 0,
controller.passwordController.text = savedPassword; bottom: 0,
_rememberPassword = true; // 如果有保存的密码,则默认勾选 child: Container(
} height: MediaQuery.of(context).size.height * 2 / 3,
_credentialsLoaded = true; // 标记为已加载,防止重复执行 decoration: BoxDecoration(
} color: Colors.white,
borderRadius: BorderRadius.only(
return Container( topLeft: Radius.circular(40),
color: Color(0xFFEFF4F7), topRight: Radius.circular(40),
child: <Widget>[ ),
Icon(cLogin ? AntdIcon.car : AntdIcon.USB), ),
SizedBox(height: 5.h), child: SingleChildScrollView(
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700), child: Padding(
SizedBox(height: 5.h), padding: EdgeInsets.symmetric(horizontal: 30.w),
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"), child: Column(
Card( crossAxisAlignment: CrossAxisAlignment.start,
shape: RoundedRectangleBorder( children: [
borderRadius: BorderRadius.circular(15), // 设置圆角弧度 SizedBox(height: 80.h),
), // 登录输入区域
margin: EdgeInsets.all(15), _buildLoginInputFields(controller),
elevation: 4, SizedBox(height: 40.h),
child: Container( // 协议
height: cLogin ? 295.h : 370.h, buildAgreement(),
padding: EdgeInsets.all(15), SizedBox(height: 80.h),
child: Column( // 底部 Slogan
children: [ Center(
Card( child: Column(
elevation: 2, children: [
child: Container( Text(
height: 55.h, "H Y P A I",
padding: EdgeInsets.all(3), style: TextStyle(
child: TabBar( fontSize: 16,
controller: tabController, fontWeight: FontWeight.w400,
onTap: (index) { color: Color.fromRGBO(51, 51, 51, 1),
delayed(300, () { letterSpacing: 8,
switchTab(index); ),
}); ),
}, Text(
labelColor: Colors.white, "HYDROGEN MOBILITY",
unselectedLabelColor: Colors.black, style: TextStyle(
indicator: BoxDecoration( fontSize: 9,
color: AppTheme.themeColor, color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(12), letterSpacing: 1,
boxShadow: [ ),
BoxShadow( ),
color: Colors.blue.withOpacity(0.2), ],
spreadRadius: 1, ),
blurRadius: 6,
), ),
], ],
), ),
tabs: [
Tab(text: '司机端登录'),
Tab(text: '加氢站登录'),
],
isScrollable: false,
), ),
), ),
), ),
Flexible( ),
child: TabBarView( if (AppTheme.is_show_host)
controller: tabController, Positioned(
children: [ top: 40.h,
_driverLoginView(controller), right: 20.w,
_stationLoginView(controller), 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 Widget _buildBackground() {
? SizedBox() return Positioned(
: Column( top: 0,
crossAxisAlignment: CrossAxisAlignment.start, 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: [ children: [
SizedBox(height: 20.h), const Text(
TextFormField( "欢迎使用 ",
controller: controller.driverIdentityController, style: TextStyle(
cursorColor: AppTheme.themeColor, fontSize: 24,
maxLength: 8, fontWeight: FontWeight.w500,
style: TextStyle(fontSize: 14), color: Color.fromRGBO(51, 51, 51, 1),
decoration: InputDecoration( ),
hintText: '请输入身份后8位', ),
border: OutlineInputBorder(), Container(
hintStyle: TextStyle(fontSize: 14), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration(
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey), color: Color.fromRGBO(56, 198, 151, 1),
focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(color: AppTheme.themeColor), ),
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<String, dynamic>,
);
//保存使用
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 Widget _buildLoginInputFields(LoginController controller) {
? SizedBox() return Column(
: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, // 手机号输入
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: [ children: [
SizedBox(height: 20), Expanded(
TextFormField( child: TextField(
controller: controller.stationIdController, inputFormatters: [
cursorColor: AppTheme.themeColor, LengthLimitingTextInputFormatter(6), // 最多6位
style: TextStyle(fontSize: 14), ],
decoration: InputDecoration( controller: controller.codeController,
hintText: '请输入加氢站编号', keyboardType: TextInputType.number,
border: OutlineInputBorder(), style: const TextStyle(fontSize: 15),
hintStyle: TextStyle(fontSize: 14), decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), hintText: '请输入验证码',
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey), hintStyle: TextStyle(color: Colors.grey, fontSize: 14),
focusedBorder: OutlineInputBorder( border: InputBorder.none,
borderSide: BorderSide(color: AppTheme.themeColor), contentPadding: EdgeInsets.symmetric(horizontal: 24),
), ),
), ),
), ),
SizedBox(height: 10), Obx(
TextFormField( () => GestureDetector(
controller: controller.passwordController, onTap: controller.countdown.value == 0
obscureText: _obscureText, ? controller.startCountdown
style: TextStyle(fontSize: 14), : null,
cursorColor: AppTheme.themeColor, child: Padding(
decoration: InputDecoration( padding: const EdgeInsets.only(right: 24.0),
hintStyle: TextStyle(fontSize: 14), child: Text(
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), controller.countdown.value == 0
hintText: '请输入密码', ? "获取验证码"
border: OutlineInputBorder(), : "${controller.countdown.value}s后重新获取",
suffixIcon: IconButton( style: TextStyle(
icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility), color: controller.countdown.value == 0
onPressed: () { ? const Color(0xFF006633)
setState(() { : Colors.grey,
_obscureText = !_obscureText; fontSize: 13,
}); fontWeight: FontWeight.bold,
}, ),
),
prefixIcon: Icon(Icons.lock_outline, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 10),
//记住密码复选框 ---
Row(
children: <Widget>[
SizedBox(
height: 24,
width: 24,
child: Checkbox(
value: _rememberPassword,
activeColor: AppTheme.themeColor,
onChanged: (bool? value) {
setState(() {
_rememberPassword = value ?? true;
});
},
), ),
), ),
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<String, dynamic>,
);
//保存使用
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; bool _isAgreed = false;
@@ -526,39 +491,4 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
Logger.d('添加别名$alias失败: $errorCode - $errorMsg'); Logger.d('添加别名$alias失败: $errorCode - $errorMsg');
} }
} }
@override
Widget build(BuildContext context) {
return GetBuilder<LoginController>(
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,
),
),
),
),
],
),
);
},
);
}
} }