Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/controller.dart
2026-01-29 19:26:59 +08:00

861 lines
26 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 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);
// 判断新选择的日期是不是今天
if (selectedDate.value.isAtSameMomentAs(today)) {
// 如果是今天,就将时间重置为当前时间所在的半小时区间
startTime.value = _calculateInitialStartTime(now);
endTime.value = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)),
);
} 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));
}
}
}
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,
);
}
// 用于存储上一次成功预约的信息
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;
}*/
final reservationEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.value.hour,
endTime.value.minute,
);
//判断预约区间的结束时间是否早于当前时间留出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: '',
rejectReason: '',
hasEdit: true,
);
//打开预约列表
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(seconds: 1), () {});
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 = "0";
String fillingWeight = "0";
String fillingTimes = "0";
String plateNumber = "";
String vin = "";
String leftHydrogen = "0";
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();
});
}
///停止定时器的方法
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',
);
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"]}";
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"].toString();
workEfficiency = result.data["workEfficiency"].toString();
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() async {
if (StorageService.to.phone == "13344444444") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
hydrogenId: '1142167389150920704',
name: '演示加氢站',
address: '上海市嘉定区于田南路111号于田大厦',
price: '35.00',
// 价格
siteStatusName: '营运中',
// 状态
isSelect: 1, // 默认可选
);
// 使用 assignAll 可以确保列表只包含这个测试数据
stationOptions.assignAll([testStation]);
if (stationOptions.isNotEmpty) {
selectedStationId.value = stationOptions.first.hydrogenId;
}
return;
}
try {
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 onDetached() {
super.onDetached();
Logger.d('aaa-onDetached');
}
@override
void onHidden() {
super.onHidden();
Logger.d('aaa-onHidden');
}
@override
void onPaused() {
super.onPaused();
Logger.d('aaa-onPaused');
stopAutoRefresh();
}
@override
void onClose() {
super.onClose();
Logger.d('aaa-onClose');
amountController.dispose();
plateNumberController.dispose();
if (_debounce != null) {
_debounce?.cancel();
}
_sheetWorker?.dispose();
stopAutoRefresh();
}
}