647 lines
20 KiB
Dart
647 lines
20 KiB
Dart
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: '',
|
||
);
|
||
|
||
//打开预约列表
|
||
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 == "13888888888") {
|
||
//该账号给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();
|
||
}
|
||
}
|