Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/controller.dart
2025-12-01 15:27:00 +08:00

724 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 '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/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 Rx<DateTime> selectedDate = DateTime.now().obs;
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
final Rx<TimeOfDay> endTime = TimeOfDay.fromDateTime(
DateTime.now().add(const Duration(minutes: 30)),
).obs;
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 formattedStartTime => _formatTimeOfDay(startTime.value);
String get formattedEndTime => _formatTimeOfDay(endTime.value);
void pickDate(BuildContext context) {
DateTime tempDate = selectedDate.value;
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: () {
selectedDate.value = tempDate;
Get.back();
},
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: DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day),
maximumDate: DateTime.now().add(const Duration(days: 365)),
onDateTimeChanged: (DateTime newDate) {
tempDate = newDate;
},
),
),
],
),
),
backgroundColor: Colors.transparent,
);
}
///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 || slotStartDateTime.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 ${formattedStartTime}:00';
if (lastSuccessfulReservation != null &&
lastSuccessfulReservation!.id == selectedStationId.value &&
lastSuccessfulReservation!.startTime == startTimeStr) {
showToast("请勿重复提交相同的预约");
return;
}
// 将选择的日期和时间组合成一个完整的 DateTime 对象
final reservationStartDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
startTime.value.hour,
startTime.value.minute,
);
// 检查预约时间是否在当前时间之前
if (reservationStartDateTime.isBefore(DateTime.now())) {
showToast("不可预约过去的时间");
return;
}
try {
final selectedStation = stationOptions.firstWhere(
(s) => s.hydrogenId == selectedStationId.value,
);
if(selectedStation.siteStatusName != "营运中"){
showToast("该站点${selectedStation.siteStatusName},暂无法预约");
return;
}
showLoading("提交中");
final dateStr = formattedDate; // "yyyy-MM-dd"
final startTimeStr = '$dateStr ${formattedStartTime}:00'; // "yyyy-MM-dd HH:mm:ss"
final endTimeStr = '$dateStr ${formattedEndTime}:00';
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: '',
);
//打开预约列表
Future.delayed(const Duration(milliseconds: 800), () {
getReservationList();
});
} else {
showErrorToast(result.message);
}
} catch (e) {
dismissLoading();
showToast('服务暂不可用,请稍后');
}
}
/// 状态变量:是否有预约数据
bool hasReservationData = false;
// 新增预约数据列表
List<ReservationModel> reservationList = [];
//查看预约列表
void getReservationList() async {
showLoading("加载中");
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/driverOrderPage",
data: {
'phone': StorageService.to.phone, // 使用从 renderData 中获取到的 name
'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条
},
);
if (response == null || response.data == null) {
showToast('暂时无法获取预约数据');
hasReservationData = false;
reservationList = [];
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['list'] ?? [];
reservationList = listFromServer.map((item) {
return ReservationModel.fromJson(item as Map<String, dynamic>);
}).toList();
// 根据列表是否为空来更新 hasReservationData 状态
hasReservationData = reservationList.isNotEmpty;
} else {
showToast(baseModel.message);
hasReservationData = false;
reservationList = []; // 清空列表
}
} catch (e) {
showToast('获取预约数据失败');
hasReservationData = false;
reservationList = []; // 清空列表
} finally {
dismissLoading();
}
Get.bottomSheet(
Container(
height: Get.height * 0.55,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
//标题
Container(
padding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'我的预约',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: !hasReservationData
? Container(
margin: EdgeInsets.only(top: 40),
child: TextX.bodyLarge('暂无预约', weight: FontWeight.w500),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: reservationList.length,
itemBuilder: (context, index) {
final ReservationModel reservation = reservationList[index];
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: const Color(
0xFFE6F7FF,
), // Light blue background
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: const Color(0xFF91D5FF),
), // Blue border
),
child: Text(
reservation.stateName,
style: const TextStyle(
color: Color(0xFF1890FF),
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(
0xFFFFF7E6,
), // Light orange background
borderRadius: BorderRadius.circular(12),
),
child: Text(
reservation.addStatusName,
style: const TextStyle(
color: Color(0xFFFA8C16),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
_buildDetailRow('联系人:', reservation.contacts),
_buildDetailRow('联系电话:', reservation.phone),
],
),
),
);
},
),
),
],
),
),
isScrollControlled: true,
backgroundColor:
Colors.transparent, // Make background transparent to see the rounded corners
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 85,
child: Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
);
}
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 {
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('站点列表已刷新');
}
} catch (e) {
showToast('数据异常');
}
} catch (e) {
} finally {
dismissLoading();
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.options.headers = originalHeaders;
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog) {
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();
super.onClose();
}
}