Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/controller.dart
2026-03-04 14:50:21 +08:00

649 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/station_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import '../../../common/styles/theme.dart';
import 'reservation_list_bottomsheet.dart';
/// Helper class for managing time slots
class TimeSlot {
final TimeOfDay start;
final TimeOfDay end;
TimeSlot(this.start, this.end);
String get display {
final startStr =
'${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}';
final endStr =
'${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}';
return '$startStr - $endStr';
}
}
class C_ReservationController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'reservation';
C_ReservationController();
final DateTime _now = DateTime.now();
// 计算当前时间属于哪个1小时区间
late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now);
late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 60)),
);
late final Rx<DateTime> selectedDate = DateTime(_now.year, _now.month, _now.day).obs;
late final Rx<TimeOfDay> startTime = _initialStartTime.obs;
late final Rx<TimeOfDay> endTime = _initialEndTime.obs;
/// 静态辅助方法,用于计算初始的开始时间
static TimeOfDay _calculateInitialStartTime(DateTime now) {
return TimeOfDay(hour: now.hour, minute: 0);
}
/// 静态辅助方法将TimeOfDay转换为DateTime
static DateTime _getDateTimeFromTimeOfDay(TimeOfDay time) {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day, time.hour, time.minute);
}
final TextEditingController amountController = TextEditingController();
TextEditingController plateNumberController = TextEditingController();
final RxList<StationModel> stationOptions = <StationModel>[].obs;
final Rxn<String> selectedStationId = Rxn<String>();
String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value);
/// 时间段
String get formattedTimeSlot =>
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
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)) {
// 如果是今天:起始时间 = max(当前小时, 营业开始小时),且上限为营业结束小时
int targetHour = now.hour;
if (targetHour < bizStartHour) targetHour = bizStartHour;
if (targetHour > bizEndHour) targetHour = bizEndHour;
startTime.value = TimeOfDay(hour: targetHour, minute: 0);
} else {
// 如果是明天:起始时间直接重置为营业开始小时
startTime.value = TimeOfDay(hour: bizStartHour, minute: 0);
}
// 结束时间默认顺延1小时
endTime.value = TimeOfDay(hour: (startTime.value.hour + 1) % 24, minute: 0);
}
// 用于存储上一次成功预约的信息
ReservationModel? lastSuccessfulReservation;
/// 提交预约
void submitReservation() async {
if (plateNumber.isEmpty) {
showToast("请先绑定车辆");
return;
}
String ampuntStr = amountController.text.toString();
if (ampuntStr.isEmpty) {
showToast("请输入需要预约的氢量");
return;
}
double ampuntDouble = (double.tryParse(ampuntStr) ?? 0.0);
if (ampuntDouble == 0) {
showToast("请输入需要预约的氢量");
return;
}
if (ampuntDouble > (double.tryParse(difference) ?? 0.0)) {
showToast('当前最大可预约氢量为${difference}(KG)');
return;
}
if (selectedStationId.value == null || selectedStationId.value!.isEmpty) {
showToast("请先选择加氢站");
return;
}
final dateStr = formattedDate;
final startTimeStr =
'$dateStr ${_formatTimeOfDay(startTime.value)}:00'; // Use helper directly
/*if (lastSuccessfulReservation != null &&
lastSuccessfulReservation!.id == selectedStationId.value &&
lastSuccessfulReservation!.startTime == startTimeStr) {
showToast("请勿重复提交相同时间段的预约,可在“查看预约”中修改");
return;
}*/
DateTime reservationEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.value.hour,
endTime.value.minute,
);
// 如果结束的小时数小于开始的小时数,或者结束时间是 00:00说明是次日
if (endTime.value.hour < startTime.value.hour ||
(endTime.value.hour == 0 && endTime.value.minute == 0)) {
reservationEndDateTime = reservationEndDateTime.add(const Duration(days: 1));
}
// 执行时间检查
if (reservationEndDateTime.isBefore(
DateTime.now().subtract(const Duration(minutes: 1)),
)) {
showToast("无法预约已过去的时间段");
return;
}
try {
final selectedStation = stationOptions.firstWhere(
(s) => s.hydrogenId == selectedStationId.value,
);
/*if (selectedStation.siteStatusName != "营运中") {
showToast("该站点${selectedStation.siteStatusName},暂无法预约");
return;
}*/
showLoading("提交中");
final endTimeStr =
'$dateStr ${_formatTimeOfDay(endTime.value)}:00'; // Use helper directly
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/saveOrUpdate',
data: {
'plateNumber': plateNumber,
'date': dateStr,
'startTime': startTimeStr,
'endTime': endTimeStr,
'stationId': selectedStationId.value,
'stationName': selectedStation.name,
'contacts': StorageService.to.name,
'phone': StorageService.to.phone,
'hydAmount': ampuntStr,
},
);
var result = BaseModel.fromJson(responseData?.data);
if (responseData == null || result.code != 0) {
dismissLoading();
showToast(result.error);
return;
}
dismissLoading();
if (result.code == 0) {
showSuccessToast("预约成功");
lastSuccessfulReservation = ReservationModel(
id: selectedStationId.value!,
stationId: '',
hydAmount: ampuntStr,
startTime: startTimeStr,
endTime: endTimeStr,
stationName: selectedStation.name,
plateNumber: '',
amount: '',
time: '',
contactPerson: '',
contactPhone: '',
contacts: '',
phone: '',
date: '',
state: '',
stateName: '',
addStatus: '',
addStatusName: '',
hasEdit: true,
rejectReason: '',
isTruckAttachment: 0,
hasHydrogenationAttachment: true,
hasDrivingAttachment: true,
isEdit: '',
drivingAttachments: [],
hydrogenationAttachments: [], gunNumber: '',
);
//打开预约列表
Future.delayed(const Duration(milliseconds: 500), () {
getReservationList(showPopup: true, addStatus: '');
});
} else {
showToast(result.error);
}
} catch (e) {
dismissLoading();
}
}
/// 状态变量:是否有预约数据
final RxBool hasReservationData = false.obs;
// 新增预约数据列表
final RxList<ReservationModel> reservationList = <ReservationModel>[].obs;
final RxBool shouldShowReservationList = false.obs;
// --- 用于防抖的 Timer ---
Timer? _debounce;
//查看预约列表
void getReservationList({bool showPopup = false, String? addStatus}) async {
// 增加 addStatus 参数
if (_debounce?.isActive ?? false) {
return;
}
_debounce = Timer(const Duration(milliseconds: 200), () {});
showLoading("加载中");
try {
final Map<String, dynamic> requestData = {
'phone': StorageService.to.phone,
'pageNum': 1,
'pageSize': 50,
};
// 将 addStatus 参数添加到请求中
if (addStatus != null && addStatus.isNotEmpty) {
requestData['addStatus'] = addStatus;
}
var response = await HttpService.to.post(
"appointment/orderAddHyd/driverOrderPage",
data: requestData,
);
if (response == null || response.data == null) {
showToast('暂时无法获取预约数据');
hasReservationData.value = false;
reservationList.clear();
return;
}
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
// 使用 .value 来更新响应式列表
reservationList.value = listFromServer.map((item) {
return ReservationModel.fromJson(item as Map<String, dynamic>);
}).toList();
// 更新 hasEdit 状态
for (var reservation in reservationList) {
try {
// 获取当前时间和预约的结束时间
final now = DateTime.now();
final endDateTime = DateTime.parse(reservation.endTime);
// 如果当前时间在结束时间之后,则不能编辑
if (now.isAfter(endDateTime) ||
plateNumber.isEmpty ||
reservation.addStatus != "0") {
reservation.hasEdit = false;
} else {
reservation.hasEdit = true;
}
} catch (e) {
reservation.hasEdit = false;
}
}
hasReservationData.value = reservationList.isNotEmpty;
if (showPopup) {
shouldShowReservationList.value = true;
}
} else {
showToast(baseModel.message);
hasReservationData.value = false;
reservationList.clear();
}
} catch (e) {
Logger.d("${e.toString()}");
showToast('获取预约数据失败');
hasReservationData.value = false;
reservationList.clear();
} finally {
dismissLoading();
}
}
String workEfficiency = "-";
String fillingWeight = "-";
String fillingTimes = "-";
String modeImage = "";
String plateNumber = "";
String vin = "";
String leftHydrogen = "-";
num maxHydrogen = 0;
String difference = "";
var progressValue = 0.0;
//用来管理查看预约的弹窗
Worker? _sheetWorker;
bool init = false;
Timer? _refreshTimer;
@override
bool get listenLifecycleEvent => true;
@override
void onInit() {
super.onInit();
getUserBindCarInfo();
getSiteList();
startAutoRefresh();
msgNotice();
if (!init) {
_setupListener();
init = true;
}
}
bool isNotice = false;
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
// 创建一个每1分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
getSiteList(showloading: false);
});
}
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
_refreshTimer = null; // 置为null方便判断
}
void _setupListener() {
_sheetWorker = ever(shouldShowReservationList, (bool shouldShow) {
if (shouldShow) {
Get.bottomSheet(
const ReservationListBottomSheet(),
isScrollControlled: true, // 允许弹窗使用更多屏幕高度
backgroundColor: Colors.transparent,
);
// 重要:显示后立即将信号重置为 false防止不必要的重复弹出
shouldShowReservationList.value = false;
}
});
}
void getUserBindCarInfo() {
if (StorageService.to.hasVehicleInfo) {
VehicleInfo? bean = StorageService.to.vehicleInfo;
if (bean == null) {
return;
}
plateNumber = bean.plateNumber;
vin = bean.vin;
plateNumberController = TextEditingController(text: plateNumber);
maxHydrogen = num.tryParse(bean.maxHydrogen) ?? 0;
getCatinfo();
getJqinfo();
}
}
void doQrCode() async {
var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) {
getUserBindCarInfo();
refreshAppui();
}
}
void getJqinfo() async {
try {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
var responseData = await HttpService.to.get(
'appointment/truck/history-filling-summary?vin=$vin&plateNumber=$plateNumber',
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
return;
}
var result = BaseModel.fromJson(responseData.data);
final value = double.tryParse(result.data["fillingWeight"]?.toString() ?? '0') ?? 0;
final String formatted = value.toStringAsFixed(2);
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
modeImage = result.data["modeImage"].toString();
updateUi();
} catch (e) {
} finally {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
}
}
void getCatinfo() async {
try {
var responseData = await HttpService.to.post(
'appointment/vehicle/getHydrogenInfoByPlateNumber',
data: {
'userName': "xll@lingniu",
'password': "4q%3!l6s0p",
'plateNumber': plateNumber,
},
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
return;
}
var result = BaseModel.fromJson(responseData.data);
leftHydrogen = "${result.data["leftHydrogen"]}Kg";
workEfficiency = "${result.data["workEfficiency"]}Kg";
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
int flooredDifference = (maxHydrogen - leftHydrogenNum).floor();
if (flooredDifference > 0) {
amountController.text = flooredDifference.toString();
}
if (maxHydrogen > 0) {
progressValue = leftHydrogenNum / maxHydrogen;
// 边界处理:确保值在 0 到 1 之间
if (progressValue > 1.0) progressValue = 1.0;
if (progressValue < 0.0) progressValue = 0.0;
}
updateUi();
} catch (e) {}
renderSliderTheme();
}
double current = 0.0;
double maxVal = 0.0;
void renderSliderTheme() {
current = double.tryParse(amountController.text) ?? 0.0;
maxVal = double.tryParse(difference) ?? 100.0;
if (maxVal <= 0) maxVal = 100.0;
updateUi();
}
void getSiteList({showloading = true}) async {
if (StorageService.to.phone == "13344444444") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
hydrogenId: '1142167389150920704',
name: '演示加氢站',
address: '上海市嘉定区于田南路111号于田大厦',
price: '35.00',
// 价格
siteStatusName: '营运中',
// 状态
isSelect: 1,
startBusiness: '08:00:00',
endBusiness: '20:00:00', // 默认可选
);
// 使用 assignAll 可以确保列表只包含这个测试数据
stationOptions.assignAll([testStation]);
if (stationOptions.isNotEmpty) {
selectedStationId.value = stationOptions.first.hydrogenId;
}
return;
}
try {
if (showloading) {
showLoading("加氢站数据加载中");
}
var responseData = await HttpService.to.get(
"appointment/station/queryHydrogenSiteInfo",
);
if (responseData == null || responseData.data == null) {
showToast('暂时无法获取站点信息');
dismissLoading();
return;
}
dismissLoading();
var result = BaseModel.fromJson(responseData.data);
var stationDataList = result.data['data'] as List;
// 使用 map 将 List<dynamic> 转换为 List<StationModel>
var stations = stationDataList
.map((item) => StationModel.fromJson(item as Map<String, dynamic>))
.toList();
// 去重,确保每个 hydrogenId 唯一
var uniqueStationsMap = <String, StationModel>{}; // 使用 Map 来去重
for (var station in stations) {
uniqueStationsMap[station.hydrogenId] = station; // 使用 hydrogenId 作为键,确保唯一
}
// 获取去重后的 List<StationModel>
var uniqueStations = uniqueStationsMap.values.toList();
stationOptions.assignAll(uniqueStations);
if (stationOptions.isEmpty) {
showToast('附近暂无可用加氢站');
} else {
// showToast('站点列表已刷新');
}
// 找到第一个可选的站点作为默认值
if (stationOptions.isNotEmpty) {
final firstSelectable = stationOptions.firstWhere(
(station) => station.isSelect == 1,
orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个
);
selectedStationId.value = firstSelectable.hydrogenId;
} else {
// 如果列表为空,确保 selectedStationId 也为空
selectedStationId.value = null;
}
} catch (e) {
dismissLoading();
showToast('数据异常');
} finally {
dismissLoading();
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog &&
StorageService.to.isLoggedIn &&
StorageService.to.loginChannel == LoginChannel.driver &&
!StorageService.to.hasVehicleInfo) {
Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆',
confirmText: "去绑定",
cancelText: "稍后",
onConfirm: () {
doQrCode();
},
);
// 标记为已显示,本次会话不再提示
StorageService.to.markBindVehicleDialogAsShown();
});
}
}
}
String _formatTimeOfDay(TimeOfDay time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
@override
void onClose() {
super.onClose();
amountController.dispose();
plateNumberController.dispose();
if (_debounce != null) {
_debounce?.cancel();
}
_sheetWorker?.dispose();
stopAutoRefresh();
}
}