Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/controller.dart

755 lines
24 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:get/get.dart';
import 'package:getx_scaffold/common/common.dart';
import 'package:getx_scaffold/common/services/http.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:intl/intl.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/c_page/reservation_edit/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import '../../../common/styles/theme.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';
final DateTime _now = DateTime.now();
// 计算当前时间属于哪个半小时区间
late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now);
late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 30)),
);
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) {
if (now.minute < 30) {
// 如果当前分钟小于30则开始时间为当前小时的0分
return TimeOfDay(hour: now.hour, minute: 0);
} else {
// 如果当前分钟大于等于30则开始时间为当前小时的30分
return TimeOfDay(hour: now.hour, minute: 30);
}
}
/// 静态辅助方法将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);
}
}
///30 分钟为间隔 时间选择器
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 < 48; i++) {
final startMinutes = i * 30;
final endMinutes = startMinutes + 30;
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
final slotStartDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
startTime.hour,
startTime.minute,
);
// 如果不是今天,所有时间段都有效
if (!isToday) {
availableSlots.add(TimeSlot(startTime, endTime));
} else {
// 如果是今天,需要判断该时间段是否可选
// 创建时间段的结束时间对象
final slotEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.hour,
endTime.minute,
);
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
if (slotEndDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(startTime, endTime));
}
}
}
if (availableSlots.isEmpty) {
showToast('今天已没有可预约的时间段');
return;
}
int initialItem = availableSlots.indexWhere(
(slot) =>
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
);
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,
},
);
if (responseData == null) {
dismissLoading();
showToast('服务暂不可用,请稍后');
return;
}
dismissLoading();
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("预约成功");
lastSuccessfulReservation = ReservationModel(
id: selectedStationId.value!,
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();
showToast('服务暂不可用,请稍后');
}
}
/// 状态变量:是否有预约数据
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) {
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 = "";
@override
bool get listenLifecycleEvent => true;
@override
void onInit() {
super.onInit();
getUserBindCarInfo();
getSiteList();
}
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 = bean.maxHydrogen;
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);
fillingWeight =
"${result.data["fillingWeight"]}${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 {
HttpService.to.setBaseUrl(AppTheme.car_service_url);
var responseData = await HttpService.to.post(
'VehicleData/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);
updateUi();
} catch (e) {
} finally {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
}
}
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;
}
showLoading("加载中");
final originalHeaders = Map<String, dynamic>.from(HttpService.to.dio.options.headers);
try {
HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url);
HttpService.to.dio.options.headers['appId'] = '97ad10eeb6b346f79e0d6ffd81e4d3c3';
var responseData = await HttpService.to.get("hydrogen/queryHydrogenSiteInfo");
if (responseData == null || responseData.data == null) {
showToast('暂时无法获取站点信息');
dismissLoading();
return;
}
try {
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) {
showToast('数据异常');
}
} catch (e) {
} finally {
dismissLoading();
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.options.headers = originalHeaders;
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) {
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() {
amountController.dispose();
plateNumberController.dispose();
if (_debounce != null) {
_debounce?.cancel();
}
super.onClose();
}
}