站点登录
This commit is contained in:
37
ln_jq_app/lib/common/login_util.dart
Normal file
37
ln_jq_app/lib/common/login_util.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,75 @@
|
||||
class BaseModel {
|
||||
bool? success;
|
||||
String? type;
|
||||
String? url;
|
||||
/// 通用的 API 响应基础模型
|
||||
/// 使用泛型 <T> 来适应 'data' 字段中任何可能的数据类型
|
||||
class BaseModel<T> {
|
||||
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(
|
||||
success: json['success']?.toString().contains("true"),
|
||||
type: json['type']?.toString(),
|
||||
url: json['url']?.toString(),
|
||||
);
|
||||
/// fromJson 工厂构造函数(重构后)
|
||||
factory BaseModel.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
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() => {
|
||||
if (success != null) 'success': success,
|
||||
if (type != null) 'type': type,
|
||||
if (url != null) 'url': url,
|
||||
};
|
||||
'code': code,
|
||||
'status': status,
|
||||
'message': message,
|
||||
'time': time,
|
||||
'data': data,
|
||||
'error': error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ class AppTheme {
|
||||
|
||||
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 darkThemeColor = Color(0xFF032896);
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import 'package:flutter/foundation.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: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/home/view.dart';
|
||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||
import 'package:ln_jq_app/storage_service.dart';
|
||||
|
||||
import 'common/styles/theme.dart';
|
||||
import 'pages/login/view.dart';
|
||||
import 'pages/home/view.dart';
|
||||
|
||||
/// Main
|
||||
void main() async {
|
||||
WidgetsBinding widgetsBinding = await init(isDebug: kDebugMode, logTag: '小羚羚');
|
||||
WidgetsBinding widgetsBinding = await init(isDebug: true, logTag: '小羚羚');
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
|
||||
await Get.putAsync(() => StorageService().init());
|
||||
initHttpSet();
|
||||
|
||||
runApp(
|
||||
GetxApp(
|
||||
// 设计稿尺寸 单位:dp
|
||||
designSize: const Size(390, 844),
|
||||
// Getx Log
|
||||
enableLog: kDebugMode,
|
||||
enableLog: true,
|
||||
// 默认的跳转动画
|
||||
defaultTransition: Transition.rightToLeft,
|
||||
// 主题模式
|
||||
@@ -30,6 +33,10 @@ void main() async {
|
||||
title: '小羚羚',
|
||||
// 首页入口
|
||||
home: HomePage(),
|
||||
// 推荐使用命名路由,如果配置好了可以取消下面两行的注释
|
||||
// initialRoute: AppPages.INITIAL,
|
||||
// getPages: AppPages.routes,
|
||||
|
||||
// Builder
|
||||
builder: (context, widget) {
|
||||
// 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 '服务器异常';
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:get/get.dart';
|
||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||
import 'package:ln_jq_app/pages/login/view.dart';
|
||||
|
||||
import '../../../storage_service.dart';
|
||||
|
||||
class ReservationController extends GetxController with BaseControllerMixin {
|
||||
@override
|
||||
String get builderId => 'b_reservation'; // 确保ID与View中一致
|
||||
@@ -50,11 +52,12 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
||||
Get.snackbar('提示', '保存成功!'); // 示例:显示一个成功的提示
|
||||
}
|
||||
|
||||
void logout() {
|
||||
void logout() async{
|
||||
// TODO: 在这里执行退出登录的逻辑
|
||||
// 1. 清理本地缓存的用户信息
|
||||
// 2. 调用退出登录接口
|
||||
// 3. 导航到登录页面
|
||||
//清理本地缓存的用户信息 导航到登录页面
|
||||
HttpService.to.clearAuthorization();
|
||||
await StorageService.to.clearLoginInfo();
|
||||
|
||||
Get.offAll(() => LoginPage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/login/view.dart';
|
||||
|
||||
import '../../storage_service.dart';
|
||||
|
||||
class HomeController extends GetxController with BaseControllerMixin {
|
||||
@override
|
||||
String get builderId => 'home';
|
||||
@@ -32,7 +34,7 @@ class HomeController extends GetxController with BaseControllerMixin {
|
||||
if (loginChannel == "b_login") {
|
||||
return BaseWidgetsPage(); // 渠道A进入 BaseWidgetsPage
|
||||
} else {
|
||||
return B_BaseWidgetsPage(); // 渠道B进入 B_BaseWidgetsPage
|
||||
return StorageService.to.isLoggedIn ? B_BaseWidgetsPage() : LoginPage(); // 渠道B进入 B_BaseWidgetsPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.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/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/login/controller.dart';
|
||||
import 'package:ln_jq_app/storage_service.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -54,76 +57,77 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||
return Container(
|
||||
color: Color(0xFFEFF4F7),
|
||||
child: <Widget>[
|
||||
Icon(cLogin ? AntdIcon.car : AntdIcon.USB),
|
||||
SizedBox(height: 5.h),
|
||||
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700),
|
||||
SizedBox(height: 5.h),
|
||||
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"),
|
||||
Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15), // 设置圆角弧度
|
||||
),
|
||||
margin: EdgeInsets.all(15),
|
||||
elevation: 4,
|
||||
child: Container(
|
||||
height: cLogin ? 260.h : 320.h,
|
||||
padding: EdgeInsets.all(15),
|
||||
child: // TabBar切换
|
||||
Column(
|
||||
children: [
|
||||
Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
onTap: (index) {
|
||||
//保证尺寸变化
|
||||
delayed(300, () {
|
||||
switchTab(index);
|
||||
});
|
||||
},
|
||||
// 修改TabBar的选中状态和未选中状态样式
|
||||
labelColor: Colors.white,
|
||||
// 选中时的文字颜色
|
||||
unselectedLabelColor: Colors.black,
|
||||
// 未选中时的文字颜色
|
||||
indicator: BoxDecoration(
|
||||
color: AppTheme.themeColor, // 选中的Tab背景色(模拟卡片式效果)
|
||||
borderRadius: BorderRadius.circular(12), // 卡片的圆角效果
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 6,
|
||||
),
|
||||
Icon(cLogin ? AntdIcon.car : AntdIcon.USB),
|
||||
SizedBox(height: 5.h),
|
||||
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700),
|
||||
SizedBox(height: 5.h),
|
||||
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"),
|
||||
Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15), // 设置圆角弧度
|
||||
),
|
||||
margin: EdgeInsets.all(15),
|
||||
elevation: 4,
|
||||
child: Container(
|
||||
height: cLogin ? 260.h : 320.h,
|
||||
padding: EdgeInsets.all(15),
|
||||
child: // TabBar切换
|
||||
Column(
|
||||
children: [
|
||||
Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
onTap: (index) {
|
||||
//保证尺寸变化
|
||||
delayed(300, () {
|
||||
switchTab(index);
|
||||
});
|
||||
},
|
||||
// 修改TabBar的选中状态和未选中状态样式
|
||||
labelColor: Colors.white,
|
||||
// 选中时的文字颜色
|
||||
unselectedLabelColor: Colors.black,
|
||||
// 未选中时的文字颜色
|
||||
indicator: BoxDecoration(
|
||||
color: AppTheme.themeColor, // 选中的Tab背景色(模拟卡片式效果)
|
||||
borderRadius: BorderRadius.circular(12), // 卡片的圆角效果
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
tabs: [
|
||||
Tab(text: '司机端登录'),
|
||||
Tab(text: '加氢站登录'),
|
||||
],
|
||||
isScrollable: false,
|
||||
),
|
||||
tabs: [
|
||||
Tab(text: '司机端登录'),
|
||||
Tab(text: '加氢站登录'),
|
||||
],
|
||||
isScrollable: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 根据选择的Tab展示不同的输入框
|
||||
Flexible(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
// 司机端登录
|
||||
_driverLoginView(controller),
|
||||
// 加氢站登录
|
||||
_stationLoginView(controller),
|
||||
],
|
||||
// 根据选择的Tab展示不同的输入框
|
||||
Flexible(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
// 司机端登录
|
||||
_driverLoginView(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(
|
||||
onPressed: () {
|
||||
// 司机端登录
|
||||
Get.offAll(() => BaseWidgetsPage());
|
||||
Get.offAll(() => BaseWidgetsPage());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.themeColor,
|
||||
minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text('登录'),
|
||||
),
|
||||
@@ -215,13 +217,60 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.themeColor,
|
||||
minimumSize: Size(double.infinity, 50), // 设置按钮宽度占满,指定最小高度
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(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('登录'),
|
||||
),
|
||||
|
||||
43
ln_jq_app/lib/storage_service.dart
Normal file
43
ln_jq_app/lib/storage_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
encrypt:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: encrypt
|
||||
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
|
||||
@@ -304,6 +304,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -35,6 +35,8 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
getx_scaffold: ^0.2.2
|
||||
encrypt: ^5.0.3
|
||||
get_storage: ^2.1.1
|
||||
|
||||
flutter_native_splash: ^2.4.7
|
||||
|
||||
|
||||
Reference in New Issue
Block a user