站点登录

This commit is contained in:
2025-11-05 10:10:47 +08:00
parent 6faf03a331
commit ea18b71523
10 changed files with 329 additions and 99 deletions

View File

@@ -0,0 +1,37 @@
import 'package:encrypt/encrypt.dart';
class LoginUtil {
// 定义密钥
// 对于ECB模式我们只需要Key不需要IV (初始化向量)
static final _keyString = '915eae87951a448c86c47796e44c1fcf';
static final _key = Key.fromUtf8(_keyString);
// 【核心修改】创建使用 ECB 模式的加密器实例
// 1. mode: AESMode.ecb -> 使用ECB模式它不需要IV。
// 2. padding: 'PKCS7' -> 在encrypt库中PKCS5和PKCS7的填充方式是兼容的。
static final _encrypter = Encrypter(AES(_key, mode: AESMode.ecb, padding: 'PKCS7'));
/// AES 加密方法
static String encrypt(String plainText) {
if (plainText.isEmpty) {
return '';
}
// 【核心修改】调用 encrypt 方法时不再需要传递 iv
final encrypted = _encrypter.encrypt(plainText);
// 返回Base64编码的密文这是网络传输的标准做法
return encrypted.base64;
}
/// AES 解密方法 (可选,如果需要解密的话)
static String decrypt(String encryptedText) {
if (encryptedText.isEmpty) {
return '';
}
final encrypted = Encrypted.fromBase64(encryptedText);
// 【核心修改】调用 decrypt 方法时不再需要传递 iv
final decrypted = _encrypter.decrypt(encrypted);
return decrypted;
}
}

View File

@@ -1,19 +1,75 @@
class BaseModel { /// 通用的 API 响应基础模型
bool? success; /// 使用泛型 <T> 来适应 'data' 字段中任何可能的数据类型
String? type; class BaseModel<T> {
String? url; final int code; // 状态码0正常其他异常
final bool status; // 状态布尔值true正常false异常
final String message; // 消息,例如 "success"
final T? data; // 核心数据,使用泛型 T可以是任何类型
final int time; // 时间戳
final dynamic error; // 错误信息,可以是任何类型或 null
BaseModel({this.success, this.type, this.url}); BaseModel({
required this.code,
required this.status,
required this.message,
this.data, // data 可以为 null
required this.time,
this.error, // error 可以为 null
});
factory BaseModel.fromJson(Map<String, dynamic> json) => BaseModel( /// fromJson 工厂构造函数(重构后)
success: json['success']?.toString().contains("true"), factory BaseModel.fromJson(
type: json['type']?.toString(), Map<String, dynamic> json, {
url: json['url']?.toString(), T? Function(dynamic dataJson)? dataBuilder,
); }) {
// 使用一个辅助函数来安全地转换类型,防止因类型不匹配(如 "0" vs 0而崩溃
int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? -1;
return -1;
}
T? finalData;
// 检查 'data' 字段是否存在
if (json.containsKey('data') && json['data'] != null) {
if (dataBuilder != null) {
// 如果提供了 dataBuilder就用它来解析成具体的 T 类型对象
finalData = dataBuilder(json['data']);
} else {
// 如果没有提供 dataBuilder但 T 不是 dynamic我们假设 data 就是 T 类型
// 这在使用 BaseModel<Map<String, dynamic>> 时很有用
if (T != dynamic) {
try {
finalData = json['data'] as T?;
} catch(e) {
// 如果直接转换失败,保持为 null避免崩溃
finalData = null;
}
} else {
// 如果 T 是 dynamic直接赋值
finalData = json['data'];
}
}
}
return BaseModel<T>(
code: _parseInt(json['code']),
status: json['status'] as bool? ?? false,
message: json['message']?.toString() ?? 'No message',
time: _parseInt(json['time']),
data: finalData,
error: json['error'],
);
}
/// toJson 方法 (可选)
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
if (success != null) 'success': success, 'code': code,
if (type != null) 'type': type, 'status': status,
if (url != null) 'url': url, 'message': message,
}; 'time': time,
'data': data,
'error': error,
};
} }

View File

@@ -7,6 +7,10 @@ class AppTheme {
static const Color themeColor = Color(0xFF0c83c3); static const Color themeColor = Color(0xFF0c83c3);
static const String test_service_url = "http://beta-esg.api.lnh2e.com/";
static const String release_service_url = "";
static const Color secondaryColor = Colors.orange; static const Color secondaryColor = Colors.orange;
static const Color darkThemeColor = Color(0xFF032896); static const Color darkThemeColor = Color(0xFF032896);

View File

@@ -1,23 +1,26 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:getx_scaffold/common/common.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart'; import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:ln_jq_app/pages/home/view.dart';
import 'common/styles/theme.dart'; import 'common/styles/theme.dart';
import 'pages/login/view.dart'; import 'pages/home/view.dart';
/// Main
void main() async { void main() async {
WidgetsBinding widgetsBinding = await init(isDebug: kDebugMode, logTag: '小羚羚'); WidgetsBinding widgetsBinding = await init(isDebug: true, logTag: '小羚羚');
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await Get.putAsync(() => StorageService().init());
initHttpSet();
runApp( runApp(
GetxApp( GetxApp(
// 设计稿尺寸 单位dp // 设计稿尺寸 单位dp
designSize: const Size(390, 844), designSize: const Size(390, 844),
// Getx Log // Getx Log
enableLog: kDebugMode, enableLog: true,
// 默认的跳转动画 // 默认的跳转动画
defaultTransition: Transition.rightToLeft, defaultTransition: Transition.rightToLeft,
// 主题模式 // 主题模式
@@ -30,6 +33,10 @@ void main() async {
title: '小羚羚', title: '小羚羚',
// 首页入口 // 首页入口
home: HomePage(), home: HomePage(),
// 推荐使用命名路由,如果配置好了可以取消下面两行的注释
// initialRoute: AppPages.INITIAL,
// getPages: AppPages.routes,
// Builder // Builder
builder: (context, widget) { builder: (context, widget) {
// do something.... // do something....
@@ -39,4 +46,23 @@ void main() async {
); );
} }
void initHttpSet() {
// 设置基础 URL
HttpService.to.setBaseUrl(AppTheme.test_service_url);
// 设置全局响应处理器
HttpService.to.setOnResponseHandler((response) async {
try {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0) {
return null;
} else {
return baseModel.message;
}
} on Exception catch (e) {
e.printInfo();
return '服务器异常';
}
});
}

View File

@@ -2,6 +2,8 @@ import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/pages/login/view.dart'; import 'package:ln_jq_app/pages/login/view.dart';
import '../../../storage_service.dart';
class ReservationController extends GetxController with BaseControllerMixin { class ReservationController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'b_reservation'; // 确保ID与View中一致 String get builderId => 'b_reservation'; // 确保ID与View中一致
@@ -50,11 +52,12 @@ class ReservationController extends GetxController with BaseControllerMixin {
Get.snackbar('提示', '保存成功!'); // 示例:显示一个成功的提示 Get.snackbar('提示', '保存成功!'); // 示例:显示一个成功的提示
} }
void logout() { void logout() async{
// TODO: 在这里执行退出登录的逻辑 // TODO: 在这里执行退出登录的逻辑
// 1. 清理本地缓存的用户信息 //清理本地缓存的用户信息 导航到登录页面
// 2. 调用退出登录接口 HttpService.to.clearAuthorization();
// 3. 导航到登录页面 await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage()); Get.offAll(() => LoginPage());
} }
} }

View File

@@ -4,6 +4,8 @@ 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/c_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/login/view.dart'; import 'package:ln_jq_app/pages/login/view.dart';
import '../../storage_service.dart';
class HomeController extends GetxController with BaseControllerMixin { class HomeController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'home'; String get builderId => 'home';
@@ -32,7 +34,7 @@ class HomeController extends GetxController with BaseControllerMixin {
if (loginChannel == "b_login") { if (loginChannel == "b_login") {
return BaseWidgetsPage(); // 渠道A进入 BaseWidgetsPage return BaseWidgetsPage(); // 渠道A进入 BaseWidgetsPage
} else { } else {
return B_BaseWidgetsPage(); // 渠道B进入 B_BaseWidgetsPage return StorageService.to.isLoggedIn ? B_BaseWidgetsPage() : LoginPage(); // 渠道B进入 B_BaseWidgetsPage
} }
} }
} }

View File

@@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; 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/login_util.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/styles/theme.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/b_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/login/controller.dart'; import 'package:ln_jq_app/pages/login/controller.dart';
import 'package:ln_jq_app/storage_service.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@@ -54,76 +57,77 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
return Container( return Container(
color: Color(0xFFEFF4F7), color: Color(0xFFEFF4F7),
child: <Widget>[ child: <Widget>[
Icon(cLogin ? AntdIcon.car : AntdIcon.USB), Icon(cLogin ? AntdIcon.car : AntdIcon.USB),
SizedBox(height: 5.h), SizedBox(height: 5.h),
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700), TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700),
SizedBox(height: 5.h), SizedBox(height: 5.h),
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"), TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"),
Card( Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), // 设置圆角弧度 borderRadius: BorderRadius.circular(15), // 设置圆角弧度
), ),
margin: EdgeInsets.all(15), margin: EdgeInsets.all(15),
elevation: 4, elevation: 4,
child: Container( child: Container(
height: cLogin ? 260.h : 320.h, height: cLogin ? 260.h : 320.h,
padding: EdgeInsets.all(15), padding: EdgeInsets.all(15),
child: // TabBar切换 child: // TabBar切换
Column( Column(
children: [ children: [
Card( Card(
elevation: 2, elevation: 2,
child: Padding( child: Padding(
padding: EdgeInsets.all(3), padding: EdgeInsets.all(3),
child: TabBar( child: TabBar(
controller: tabController, controller: tabController,
onTap: (index) { onTap: (index) {
//保证尺寸变化 //保证尺寸变化
delayed(300, () { delayed(300, () {
switchTab(index); switchTab(index);
}); });
}, },
// 修改TabBar的选中状态和未选中状态样式 // 修改TabBar的选中状态和未选中状态样式
labelColor: Colors.white, labelColor: Colors.white,
// 选中时的文字颜色 // 选中时的文字颜色
unselectedLabelColor: Colors.black, unselectedLabelColor: Colors.black,
// 未选中时的文字颜色 // 未选中时的文字颜色
indicator: BoxDecoration( indicator: BoxDecoration(
color: AppTheme.themeColor, // 选中的Tab背景色模拟卡片式效果 color: AppTheme.themeColor, // 选中的Tab背景色模拟卡片式效果
borderRadius: BorderRadius.circular(12), // 卡片的圆角效果 borderRadius: BorderRadius.circular(12), // 卡片的圆角效果
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.blue.withOpacity(0.2), color: Colors.blue.withOpacity(0.2),
spreadRadius: 1, spreadRadius: 1,
blurRadius: 6, blurRadius: 6,
), ),
],
),
tabs: [
Tab(text: '司机端登录'),
Tab(text: '加氢站登录'),
], ],
isScrollable: false,
), ),
tabs: [
Tab(text: '司机端登录'),
Tab(text: '加氢站登录'),
],
isScrollable: false,
), ),
), ),
), // 根据选择的Tab展示不同的输入框
// 根据选择的Tab展示不同的输入框 Flexible(
Flexible( child: TabBarView(
child: TabBarView( controller: tabController,
controller: tabController, children: [
children: [ // 司机端登录
// 司机端登录 _driverLoginView(controller),
_driverLoginView(controller), // 加氢站登录
// 加氢站登录 _stationLoginView(controller),
_stationLoginView(controller), ],
], ),
), ),
), ],
], ),
), ),
), ),
), ].toColumn(mainAxisSize: MainAxisSize.min).center(),
].toColumn(mainAxisSize: MainAxisSize.min).center(),); );
} }
// 司机端登录界面 // 司机端登录界面
@@ -151,14 +155,12 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
// 司机端登录 // 司机端登录
Get.offAll(() => BaseWidgetsPage()); Get.offAll(() => BaseWidgetsPage());
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.themeColor, backgroundColor: AppTheme.themeColor,
minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度 minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
borderRadius: BorderRadius.circular(12),
),
), ),
child: Text('登录'), child: Text('登录'),
), ),
@@ -215,13 +217,60 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.themeColor, backgroundColor: AppTheme.themeColor,
minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度 minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
borderRadius: BorderRadius.circular(12),
),
), ),
onPressed: () { onPressed: () async {
// 加氢站登录逻辑 // 加氢站登录逻辑
Get.offAll(() => B_BaseWidgetsPage()); String account = controller.stationIdController.text;
String password = controller.passwordController.text;
//todo 删除
account = "000017";
password = "LnQn.314000";
if (account.isEmpty || password.isEmpty) {
showToast("请输入账号和密码");
return;
}
showLoading('登录中...');
try {
// 对密码进行AES加密
String encryptedPassword = LoginUtil.encrypt(password);
// 调用登录接口
var responseData = await HttpService.to.post(
'/login/password',
data: {
'account': account,
'password': encryptedPassword,
'loginType': "station",
},
);
if (responseData == null && responseData!.data == null) {
dismissLoading();
showToast('登录失败:无法获取凭证');
return;
}
final responseMap = responseData.data as Map<String, dynamic>;
//保存用户信息
String token = responseMap['token'] ?? '';
//hydrogenId
String userId = responseMap['userId'] ?? '';
await StorageService.to.saveLoginInfo(token: token, userId: userId);
dismissLoading();
showToast('登录成功,欢迎您');
HttpService.to.setAuthorization(token);
// 跳转到主页,并清除所有历史页面
Get.offAll(() => B_BaseWidgetsPage());
} catch (e) {
dismissLoading();
}
}, },
child: Text('登录'), child: Text('登录'),
), ),

View File

@@ -0,0 +1,43 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class StorageService extends GetxService {
late final GetStorage _box;
// 定义存储时使用的键名Key
static const String _tokenKey = 'user_token';
static const String _userIdKey = 'user_id';
// 提供一个静态的 'to' 方法,方便全局访问
static StorageService get to => Get.find();
// Service 初始化
Future<StorageService> init() async {
_box = GetStorage();
return this;
}
/// 判断是否已登录 (通过检查 token 是否存在且不为空)
bool get isLoggedIn =>
_box
.read<String?>(_tokenKey)
?.isNotEmpty ?? false;
/// 获取 Token
String? get token => _box.read<String?>(_tokenKey);
/// 获取 UserId
String? get userId => _box.read<String?>(_userIdKey);
/// 保存用户信息
Future<void> saveLoginInfo({required String token, required String userId}) async {
await _box.write(_tokenKey, token);
await _box.write(_userIdKey, userId);
}
/// 3. 删除用户信息 (用于退出登录)
Future<void> clearLoginInfo() async {
await _box.remove(_tokenKey);
await _box.remove(_userIdKey);
}
}

View File

@@ -178,7 +178,7 @@ packages:
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
encrypt: encrypt:
dependency: transitive dependency: "direct main"
description: description:
name: encrypt name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
@@ -304,6 +304,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.7.2" version: "4.7.2"
get_storage:
dependency: "direct main"
description:
name: get_storage
sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
getx_scaffold: getx_scaffold:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -35,6 +35,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
getx_scaffold: ^0.2.2 getx_scaffold: ^0.2.2
encrypt: ^5.0.3
get_storage: ^2.1.1
flutter_native_splash: ^2.4.7 flutter_native_splash: ^2.4.7