5 Commits

Author SHA1 Message Date
756bf53cf5 司机预约时间调整 2026-02-06 14:16:26 +08:00
f68c2d0938 未车辆显示 2026-02-05 14:50:03 +08:00
211d0225e4 车辆图片动态 2026-02-05 13:54:10 +08:00
7d9b4d99e8 应用更新 2026-02-05 10:30:31 +08:00
3dd583a278 401增加节流 2026-02-03 10:59:05 +08:00
15 changed files with 315 additions and 343 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 545 KiB

View File

@@ -12,6 +12,8 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -50,6 +52,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
@@ -80,6 +83,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash:
@@ -111,6 +116,7 @@ SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf

View File

@@ -0,0 +1,22 @@
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/pages/login/view.dart';
import 'package:ln_jq_app/storage_service.dart';
class AuthGuard {
static bool _handling401 = false;
static Future<void> handle401(String? message) async {
if (_handling401) return;
_handling401 = true;
try {
await StorageService.to.clearLoginInfo();
Get.offAll(() => const LoginPage());
} finally {
// 防止意外卡死,可视情况是否延迟重置
Future.delayed(const Duration(seconds: 1), () {
_handling401 = false;
});
}
}
}

View File

@@ -4,7 +4,9 @@ class StationModel {
final String address;
final String price;
final String siteStatusName; // 例如 "维修中"
final int isSelect; // 新增字段 1是可用 0是不可用
final int isSelect; // 1是可用 0是不可用
final String startBusiness; // 新增:可预约最早开始时间,如 "06:00:00"
final String endBusiness; // 新增:可预约最晚结束时间,如 "22:00:00"
StationModel({
required this.hydrogenId,
@@ -13,9 +15,10 @@ class StationModel {
required this.price,
required this.siteStatusName,
required this.isSelect,
required this.startBusiness,
required this.endBusiness,
});
// 从 JSON map 创建对象的工厂构造函数
factory StationModel.fromJson(Map<String, dynamic> json) {
return StationModel(
hydrogenId: json['hydrogenId'] ?? '',
@@ -23,7 +26,9 @@ class StationModel {
address: json['address'] ?? '地址未知',
price: json['price']?.toString() ?? '0.00',
siteStatusName: json['siteStatusName'] ?? '',
isSelect: json['isSelect'] as int? ?? 0, // 新增字段的解析,默认为 0
isSelect: json['isSelect'] as int? ?? 0,
startBusiness: json['startBusiness'] ?? '00:00:00', // 默认全天
endBusiness: json['endBusiness'] ?? '23:59:59', // 默认全天
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/AuthGuard.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/token_interceptor.dart';
import 'package:ln_jq_app/storage_service.dart';
@@ -20,7 +21,7 @@ void main() async {
logTag: '小羚羚',
supportedLocales: [const Locale('zh', 'CN')],
);
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -65,6 +66,7 @@ void main() async {
void initHttpSet() {
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
HttpService.to.init(timeout: 15);
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
HttpService.to.setOnResponseHandler((response) async {
@@ -73,8 +75,7 @@ void initHttpSet() {
if (baseModel.code == 0 || baseModel.code == 200) {
return null;
} else if (baseModel.code == 401) {
await StorageService.to.clearLoginInfo();
Get.offAll(() => const LoginPage());
await AuthGuard.handle401(baseModel.message);
return baseModel.message;
} else {
return (baseModel.error.toString()).isEmpty

View File

@@ -15,9 +15,9 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// --- 车辆基本信息 ---
String plateNumber = "";
String vin = "未知";
String modelName = "未知";
String brandName = "未知";
String vin = "-";
String modelName = "-";
String brandName = "-";
// --- 证件附件列表 ---
final RxList<String> drivingAttachments = <String>[].obs;

View File

@@ -133,7 +133,7 @@ class CarInfoPage extends GetView<CarInfoController> {
),
),
IconButton(
onPressed: () async{
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
@@ -163,11 +163,26 @@ class CarInfoPage extends GetView<CarInfoController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
_buildModernStatItem(
'本月里程数',
'Accumulated',
StorageService.to.hasVehicleInfo ? '2,852km' : '-',
'',
),
const SizedBox(width: 8),
_buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
_buildModernStatItem(
'总里程',
'Refuel Count',
StorageService.to.hasVehicleInfo ? "2.5W km" : '-',
'',
),
const SizedBox(width: 8),
_buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
_buildModernStatItem(
'服务评分',
'Driver rating',
StorageService.to.hasVehicleInfo ? "4.9分" : '-',
'',
),
],
),
),
@@ -300,20 +315,20 @@ class CarInfoPage extends GetView<CarInfoController> {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: const LinearProgressIndicator(
value: 0.75,
child: LinearProgressIndicator(
value: StorageService.to.hasVehicleInfo ? 0.75 : 0,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
const Row(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
"75%",
StorageService.to.hasVehicleInfo ? "75%" : "0%",
style: TextStyle(
fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
@@ -382,7 +397,11 @@ class CarInfoPage extends GetView<CarInfoController> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
_buildCertDetailItem(
'所属公司',
controller.rentFromCompany,
isFull: false,
),
_buildCertDetailItem('运营城市', controller.address),
],
),

View File

@@ -69,239 +69,34 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
String get formattedTimeSlot =>
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
void pickDate(BuildContext context) {
DateTime tempDate = selectedDate.value;
// 获取今天的日期 (不含时间)
final DateTime today = DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
//计算明天的日期
final DateTime tomorrow = today.add(const Duration(days: 1));
Get.bottomSheet(
Container(
height: 300,
padding: const EdgeInsets.only(top: 6.0),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
onPressed: () => Get.back(),
child: const Text(
'取消',
style: TextStyle(color: CupertinoColors.systemGrey),
),
),
CupertinoButton(
onPressed: () {
final bool isChangingToToday =
tempDate.isAtSameMomentAs(today) &&
!selectedDate.value.isAtSameMomentAs(today);
final bool isDateChanged = !tempDate.isAtSameMomentAs(
selectedDate.value,
);
// 更新选中的日期
selectedDate.value = tempDate;
Get.back(); // 先关闭弹窗
// 如果日期发生了变化,则重置时间
if (isDateChanged) {
resetTimeForSelectedDate();
}
},
child: const Text(
'确认',
style: TextStyle(
color: AppTheme.themeColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFE5E5E5)),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: selectedDate.value,
minimumDate: today,
// 最小可选日期为今天
maximumDate: tomorrow,
// 最大可选日期为明天
// ---------------------
onDateTimeChanged: (DateTime newDate) {
tempDate = newDate;
},
),
),
],
),
),
backgroundColor: Colors.transparent,
);
}
void resetTimeForSelectedDate() {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
// 判断新选择的日期是不是今天
// 1. 获取当前站点
final station = stationOptions.firstWhereOrNull(
(s) => s.hydrogenId == selectedStationId.value,
);
if (station == null) return;
// 2. 解析营业开始和结束的小时
final bizStartHour = int.tryParse(station.startBusiness.split(':')[0]) ?? 0;
final bizEndHour = int.tryParse(station.endBusiness.split(':')[0]) ?? 23;
if (selectedDate.value.isAtSameMomentAs(today)) {
// 如果是今天,就将时间重置为当前时间所在的半小时区间
startTime.value = _calculateInitialStartTime(now);
endTime.value = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)),
);
// 如果是今天:起始时间 = max(当前小时, 营业开始小时),且上限为营业结束小时
int targetHour = now.hour;
if (targetHour < bizStartHour) targetHour = bizStartHour;
if (targetHour > bizEndHour) targetHour = bizEndHour;
startTime.value = TimeOfDay(hour: targetHour, minute: 0);
} else {
// 如果是明天(或其他未来日期),则可以将时间重置为一天的最早可用时间,例如 00:00
startTime.value = const TimeOfDay(hour: 0, minute: 0);
endTime.value = const TimeOfDay(hour: 0, minute: 30);
}
}
///60 分钟为间隔 时间选择器
void pickTime(BuildContext context) {
final now = DateTime.now();
final isToday =
selectedDate.value.year == now.year &&
selectedDate.value.month == now.month &&
selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = [];
for (int i = 0; i < 24; i++) {
// 每次增加 60 分钟
final startMinutes = i * 60;
final endMinutes = startMinutes + 60;
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
// 注意endMinutes % 60 始终为 0因为间隔是整小时
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
// 如果不是今天,所有时间段都有效
if (!isToday) {
availableSlots.add(TimeSlot(startTime, endTime));
} else {
// 如果是今天,需要判断该时间段是否可选
// 创建时间段的结束时间对象
final slotEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.hour,
endTime.minute,
);
// 注意:如果是跨天的 00:00 (例如 23:00 - 00:00),需要将日期加一天,否则 isAfter 判断会出错
// 但由于我们用的是 endTime.hour % 24当变成 0 时,日期还是 selectedDate
// 这里做一个特殊处理:如果 endTime 是 00:00意味着它实际上是明天的开始
DateTime realEndDateTime = slotEndDateTime;
if (endTime.hour == 0 && endTime.minute == 0) {
realEndDateTime = slotEndDateTime.add(const Duration(days: 1));
}
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
if (realEndDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(startTime, endTime));
}
}
// 如果是明天:起始时间直接重置为营业开始小时
startTime.value = TimeOfDay(hour: bizStartHour, minute: 0);
}
if (availableSlots.isEmpty) {
showToast('今天已没有可预约的时间段');
return;
}
// 查找当前选中的时间对应的新列表中的索引
int initialItem = availableSlots.indexWhere(
(slot) => slot.start.hour == startTime.value.hour,
);
if (initialItem == -1) {
initialItem = 0;
}
TimeSlot tempSlot = availableSlots[initialItem];
final FixedExtentScrollController scrollController = FixedExtentScrollController(
initialItem: initialItem,
);
Get.bottomSheet(
Container(
height: 300,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
onPressed: () => Get.back(),
child: const Text(
'取消',
style: TextStyle(color: CupertinoColors.systemGrey),
),
),
CupertinoButton(
onPressed: () {
startTime.value = tempSlot.start;
endTime.value = tempSlot.end;
Get.back();
},
child: const Text(
'确认',
style: TextStyle(
color: AppTheme.themeColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFE5E5E5)),
Expanded(
child: CupertinoPicker(
scrollController: scrollController,
itemExtent: 40.0,
onSelectedItemChanged: (index) {
tempSlot = availableSlots[index];
},
children: availableSlots
.map((slot) => Center(child: Text(slot.display)))
.toList(),
),
),
],
),
),
backgroundColor: Colors.transparent,
);
// 结束时间默认顺延1小时
endTime.value = TimeOfDay(hour: (startTime.value.hour + 1) % 24, minute: 0);
}
// 用于存储上一次成功预约的信息
@@ -536,12 +331,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
}
String workEfficiency = "0";
String fillingWeight = "0";
String fillingTimes = "0";
String workEfficiency = "-";
String fillingWeight = "-";
String fillingTimes = "-";
String modeImage = "";
String plateNumber = "";
String vin = "";
String leftHydrogen = "0";
String leftHydrogen = "-";
num maxHydrogen = 0;
String difference = "";
var progressValue = 0.0;
@@ -650,7 +446,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
try {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
var responseData = await HttpService.to.get(
'appointment/truck/history-filling-summary?vin=$vin',
'appointment/truck/history-filling-summary?vin=$vin&plateNumber=$plateNumber',
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
@@ -664,6 +460,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
modeImage = result.data["modeImage"].toString();
updateUi();
} catch (e) {
@@ -689,8 +486,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data);
leftHydrogen = result.data["leftHydrogen"].toString();
workEfficiency = result.data["workEfficiency"].toString();
leftHydrogen = "${result.data["leftHydrogen"]}Kg";
workEfficiency = "${result.data["workEfficiency"]}Kg";
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
@@ -725,7 +522,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
void getSiteList() async {
if (StorageService.to.phone == "13344444444") {
if (StorageService.to.phone == "13888888888") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
hydrogenId: '1142167389150920704',
@@ -735,7 +532,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
// 价格
siteStatusName: '营运中',
// 状态
isSelect: 1, // 默认可选
isSelect: 1,
startBusiness: '08:00:00',
endBusiness: '20:00:00', // 默认可选
);
// 使用 assignAll 可以确保列表只包含这个测试数据
stationOptions.assignAll([testStation]);

View File

@@ -148,9 +148,11 @@ class ReservationPage extends GetView<C_ReservationController> {
),
IconButton(
onPressed: () async {
controller.stopAutoRefresh();
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
controller.startAutoRefresh();
}
},
icon: Badge(
@@ -177,7 +179,12 @@ class ReservationPage extends GetView<C_ReservationController> {
const SizedBox(width: 8),
_buildModernStatItem('总加氢次数', '', controller.fillingTimes, ''),
const SizedBox(width: 8),
_buildModernStatItem('今日里程', '', "7kg", ''),
_buildModernStatItem(
'今日里程',
'',
StorageService.to.hasVehicleInfo ? "7kg" : "-",
'',
),
],
),
),
@@ -237,17 +244,33 @@ class ReservationPage extends GetView<C_ReservationController> {
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(flex: 4, child: LoginUtil.getAssImg('ic_car_bg@2x')),
Expanded(
flex: 4,
child: Image.network(
controller.modeImage,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) {
return Center(child: LoginUtil.getAssImg('ic_car_select@2x'));
},
),
),
const SizedBox(width: 16),
Expanded(
flex: 6,
child: Column(
children: [
_buildCarDataItem('剩余电量', '36.8%'),
_buildCarDataItem(
'剩余电量',
StorageService.to.hasVehicleInfo ? '36.8%' : '-',
),
const SizedBox(height: 8),
_buildCarDataItem('剩余氢量', '${controller.leftHydrogen}Kg'),
_buildCarDataItem('剩余氢量', controller.leftHydrogen),
const SizedBox(height: 8),
_buildCarDataItem('百公里氢耗', '${controller.workEfficiency}Kg'),
_buildCarDataItem('百公里氢耗', controller.workEfficiency),
const SizedBox(height: 12),
Column(
children: [
@@ -275,7 +298,7 @@ class ReservationPage extends GetView<C_ReservationController> {
),
),
Text(
"${controller.leftHydrogen}Kg",
controller.leftHydrogen,
style: const TextStyle(
fontSize: 10,
color: Color(0xFF006633),
@@ -434,8 +457,27 @@ class ReservationPage extends GetView<C_ReservationController> {
/// 时间 Slider 选择器
Widget _buildTimeSlider(BuildContext context) {
return Obx(() {
// 这里的逻辑对应 Controller 中的 24 小时可用 Slot
int currentIdx = controller.startTime.value.hour;
// 获取当前小时作为滑块值 (0-23)
int currentHour = controller.startTime.value.hour;
// 动态获取站点的营业范围限制
final station = controller.stationOptions.firstWhereOrNull(
(s) => s.hydrogenId == controller.selectedStationId.value,
);
// 解析营业时间
// 处理格式如 "09:00" 或 "09:00:00"
final startParts = (station?.startBusiness ?? "00:00").split(':');
final endParts = (station?.endBusiness ?? "23:59").split(':');
int bizStartHour = int.tryParse(startParts[0]) ?? 0;
int bizEndHour = int.tryParse(endParts[0]) ?? 23;
int bizEndMinute = (endParts.length > 1) ? (int.tryParse(endParts[1]) ?? 0) : 0;
// 优化结束小时逻辑
// 如果分钟为 0 (例如 18:00),说明该小时整点已关门,最大可选小时应减 1
if (bizEndMinute == 0 && bizEndHour > 0) {
bizEndHour--;
}
return Column(
children: [
@@ -484,23 +526,23 @@ class ReservationPage extends GetView<C_ReservationController> {
overlayColor: const Color(0xFF006633).withOpacity(0.1),
),
child: Slider(
value: currentIdx.toDouble(),
min: 0,
max: 23,
divisions: 23,
value: currentHour.toDouble().clamp(
bizStartHour.toDouble(),
bizEndHour.toDouble(),
),
min: bizStartHour.toDouble(),
max: bizEndHour.toDouble(),
// divisions: bizEndHour - bizStartHour > 0 ? bizEndHour - bizStartHour : 1,
onChanged: (val) {
int hour = val.toInt();
// 模拟 Controller 中的 pickTime 逻辑校验
final now = DateTime.now();
final isToday =
controller.selectedDate.value.year == now.year &&
controller.selectedDate.value.month == now.month &&
controller.selectedDate.value.day == now.day;
if (isToday && hour < now.hour) {
// 如果是今天且小时数小于当前,则忽略
return;
}
// 如果是今天,判断不可选过去的时间点
if (isToday && hour < now.hour) return;
controller.startTime.value = TimeOfDay(hour: hour, minute: 0);
controller.endTime.value = TimeOfDay(hour: (hour + 1) % 24, minute: 0);
@@ -627,6 +669,7 @@ class ReservationPage extends GetView<C_ReservationController> {
onChanged: (value) {
if (value != null) {
controller.selectedStationId.value = value;
controller.resetTimeForSelectedDate();
}
},

View File

@@ -2,8 +2,11 @@ import 'dart:io';
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:flutter_app_update/result_model.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:getx_scaffold/getx_scaffold.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';
@@ -20,107 +23,195 @@ class HomeController extends GetxController with BaseControllerMixin {
final _aliyunPush = AliyunPushFlutter();
@override
bool get listenLifecycleEvent => true;
@override
void onInit() {
super.onInit();
initAliyunPush();
addPushCallback();
FlutterNativeSplash.remove();
log('page-init');
// 页面初始化后执行版本检查
checkVersionInfo();
}
String downloadUrl = "";
/// 检查 App 更新信息,增加版本号比对逻辑
void checkVersionInfo() async {
try {
final response = await HttpService.to.get('appointment/appConfig/get');
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
final data = result.data as Map<String, dynamic>;
bool hasUpdate = data['hasUpdate']?.toString().toLowerCase() == "true";
bool isForce = data['isForce']?.toString().toLowerCase() == "true";
String versionName = data['versionName'] ?? "新版本";
String updateContent = data['updateContent'] ?? "优化系统性能,提升用户体验";
downloadUrl = data['downloadUrl'].toString();
// 获取服务器配置的目标构建号
int serverVersionCode =
int.tryParse(data['versionCode']?.toString() ?? "0") ?? 0;
int serverIosBuildId = int.tryParse(data['iosBuildId']?.toString() ?? "0") ?? 0;
// 获取本地当前的构建号
String currentBuildStr = await getBuildNumber();
int currentBuild = int.tryParse(currentBuildStr) ?? 0;
bool needUpdate = false;
if (GetPlatform.isAndroid) {
needUpdate = currentBuild < serverVersionCode;
} else if (GetPlatform.isIOS) {
needUpdate = currentBuild < serverIosBuildId;
}
// 如果服务器标记有更新,且本地版本号确实较低,则弹出更新
if (hasUpdate && needUpdate) {
_showUpdateDialog("版本:$versionName\n\n更新内容:\n$updateContent", isForce);
}
}
}
} catch (e) {
Logger.d("版本检查失败: $e");
}
}
/// 显示更新弹窗
void _showUpdateDialog(String content, bool isForce) {
DialogX.to.showConfirmDialog(
title: '升级提醒',
confirmText: '立即升级',
content: _buildDialogContent(content),
// 如果是强制更新,取消按钮显示为空,即隐藏
cancelText: isForce ? "" : '以后再说',
// 设置为 false禁止点击背景和物理返回键关闭
barrierDismissible: false,
onConfirm: () {
jumpUpdateApp();
// ios如果是强制更新点击后维持弹窗防止用户进入 App
if (isForce && GetPlatform.isIOS) {
Future.delayed(const Duration(milliseconds: 500), () {
_showUpdateDialog(content, isForce);
});
}
},
);
}
Widget _buildDialogContent(String content) {
return PopScope(
canPop: false, // 关键:禁止 pop
child: TextX.bodyMedium(content).padding(bottom: 16.h),
);
}
void jumpUpdateApp() {
if (GetPlatform.isIOS) {
// 跳转到 iOS 应用商店网页
openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815");
} else if (GetPlatform.isAndroid) {
// Android 执行下载安装流程
showAndroidDownloadDialog();
}
}
void showAndroidDownloadDialog() {
AzhonAppUpdate.listener((ResultModel model) {
if (model.type == ResultType.start) {
DialogX.to.showConfirmDialog(
content: PopScope(
canPop: false,
child: Center(
child: Column(
children: [
TextX.bodyMedium('升级中...').padding(bottom: 45.h),
CircularProgressIndicator(),
],
),
),
),
confirmText: '',
cancelText: "",
barrierDismissible: false,
);
} else if (model.type == ResultType.done) {
Get.back();
}
});
UpdateModel model = UpdateModel(downloadUrl, "xll.apk", "logo", '正在下载最新版本...');
AzhonAppUpdate.update(model);
}
// 根据登录状态和登录渠道返回不同的首页
Widget getHomePage() {
requestPermission();
//登录状态跳转
if (StorageService.to.isLoggedIn) {
// 如果已登录,再判断是哪个渠道
if (StorageService.to.loginChannel == LoginChannel.station) {
return B_BaseWidgetsPage(); // 站点首页
return B_BaseWidgetsPage();
} else if (StorageService.to.loginChannel == LoginChannel.driver) {
return BaseWidgetsPage(); // 司机首页
return BaseWidgetsPage();
} else {
return LoginPage();
}
} else {
// 未登录,直接去登录页
return LoginPage();
}
}
void requestPermission() async {
PermissionStatus status = await Permission.notification.status;
if (status.isGranted) {
Logger.d("通知权限已开启");
return;
}
if (status.isGranted) return;
if (status.isDenied) {
// 建议此处增加一个应用内的 Rationale (解释说明) 弹窗
status = await Permission.notification.request();
}
if (status.isGranted) {
// 授权成功
Logger.d('通知已开启');
} else if (status.isPermanentlyDenied) {
Logger.d('通知权限已被拒绝,请到系统设置中开启');
} else if (status.isDenied) {
Logger.d('请授予通知权限,以便接收加氢站通知');
}
}
///推送相关
///推送相关初始化 (保持原样)
Future<void> initAliyunPush() async {
// 1. 配置分离:建议将 Key 提取到外部或配置文件中
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
final String appSecret = Platform.isIOS
? AppTheme.ios_appsecret
: AppTheme.android_appsecret;
try {
// 初始化推送
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
if (result['code'] != kAliyunPushSuccessCode) {
Logger.d('初始化推送失败: ${result['code']} - ${result['errorMsg']}');
return;
}
Logger.d('阿里云推送初始化成功');
// 分平台配置
if (result['code'] != kAliyunPushSuccessCode) return;
if (Platform.isIOS) {
await _setupIOSConfig();
} else if (Platform.isAndroid) {
await _setupAndroidConfig();
}
} catch (e) {
Logger.d('初始化过程中发生异常: $e');
Logger.d('初始化异常: $e');
}
}
/// iOS 专属配置
Future<void> _setupIOSConfig() async {
final res = await _aliyunPush.showIOSNoticeWhenForeground(true);
if (res['code'] == kAliyunPushSuccessCode) {
Logger.d('iOS 前台通知展示已开启');
} else {
Logger.d('iOS 前台通知开启失败: ${res['errorMsg']}');
}
await _aliyunPush.showIOSNoticeWhenForeground(true);
}
/// Android 专属配置
Future<void> _setupAndroidConfig() async {
await _aliyunPush.setNotificationInGroup(true);
final res = await _aliyunPush.createAndroidChannel(
await _aliyunPush.createAndroidChannel(
"xll_push_android",
'新消息通知',
4,
'用于接收加氢站实时状态提醒',
);
if (res['code'] == kAliyunPushSuccessCode) {
Logger.d('Android 通知通道创建成功');
} else {
Logger.d('Android 通道创建失败: ${res['code']} - ${res['errorMsg']}');
}
}
void addPushCallback() {
@@ -139,40 +230,23 @@ class HomeController extends GetxController with BaseControllerMixin {
Future<void> _onAndroidNotificationClickedWithNoAction(
Map<dynamic, dynamic> message,
) async {
Logger.d('onAndroidNotificationClickedWithNoAction ====> $message');
}
) async {}
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {
Logger.d('onAndroidNotificationReceivedInApp ====> $message');
}
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {}
Future<void> _onMessage(Map<dynamic, dynamic> message) async {
Logger.d('onMessage ====> $message');
}
Future<void> _onMessage(Map<dynamic, dynamic> message) async {}
Future<void> _onNotification(Map<dynamic, dynamic> message) async {
Logger.d('onNotification ====> $message');
}
Future<void> _onNotification(Map<dynamic, dynamic> message) async {}
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
Logger.d('onNotificationOpened ====> $message');
await Get.to(() => const MessagePage());
}
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {
Logger.d('onNotificationRemoved ====> $message');
}
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {}
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {
Logger.d('onIOSChannelOpened ====> $message');
}
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {}
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {
Logger.d('onIOSRegisterDeviceTokenSuccess ====> $message');
}
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {}
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {
Logger.d('onIOSRegisterDeviceTokenFailed====> $message');
}
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {}
}

View File

@@ -5,18 +5,13 @@ import 'package:ln_jq_app/pages/home/controller.dart';
class HomePage extends GetView<HomeController> {
const HomePage({super.key});
// 主视图
Widget _buildView() {
return <Widget>[Text('主页面')].toColumn(mainAxisSize: MainAxisSize.min).center();
}
@override
Widget build(BuildContext context) {
return GetBuilder<HomeController>(
init: HomeController(),
id: 'home',
builder: (_) {
return controller.getHomePage();
return Scaffold(body: controller.getHomePage());
},
);
}

View File

@@ -204,9 +204,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
Row(
children: [
const Text(
"欢迎使用 ",
"欢迎使用小羚羚 ",
style: TextStyle(
fontSize: 24,
fontSize: 22,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(51, 51, 51, 1),
),

View File

@@ -23,7 +23,7 @@ class WelcomePage extends GetView<WelcomeController> {
right: 0,
child: Image.asset(
'assets/images/welcome.png',
fit: BoxFit.fill
fit: BoxFit.cover
),
),
],

View File

@@ -302,6 +302,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_app_update:
dependency: "direct main"
description:
name: flutter_app_update
sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.2"
flutter_easyloading:
dependency: transitive
description:

View File

@@ -52,7 +52,7 @@ dependencies:
geolocator: ^14.0.2 # 获取精确定位
aliyun_push_flutter: ^1.3.6
pull_to_refresh: ^2.0.0
flutter_app_update: ^3.2.2
dev_dependencies: