Files
ln-ios/ln_jq_app/lib/pages/login/view.dart
2026-01-29 17:01:21 +08:00

654 lines
20 KiB
Dart

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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> 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<LoginController>(
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),
],
),
),
),
],
),
),
),
Positioned(left: 0, right: 0, bottom: 33.h, child: _buildFooterSlogan()),
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,
alignment: Alignment.center,
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'],
);
// 成功后自动获取车辆信息
try {
var carInfo = await HttpService.to.get(
"appointment/driver/getTruckInfoByDriver?phone=${result.data['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);
}
}
} catch (e) {
Logger.d("暂时不处理 查询车辆信息失败的情况");
}
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 {
var result = await _aliyunPush.bindAccount(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 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,
),
),
],
),
);
}
}