Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/controller.dart
2025-11-14 17:14:58 +08:00

713 lines
25 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/storage_service.dart';
import '../../../common/styles/theme.dart';
class 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>(); // 用 ID 来选择
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.isBefore(
DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
),
)
? DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
)
: selectedDate.value,
// 设置最小可选日期为“今天凌晨0点”
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,
);
}
void pickTime(BuildContext context, bool isStartTime) {
// 确定当前操作的时间和初始值
TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value;
DateTime now = DateTime.now();
// 计算最小可选时间
DateTime? minimumDateTime; // 默认为 null即可选任意时间
// 获取当前选择的日期(年月日)
final selectedDay = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
);
// 获取今天的日期(年月日)
final today = DateTime(now.year, now.month, now.day);
if (isStartTime) {
// 如果是选择开始时间并且日期是 今天
if (selectedDay.isAtSameMomentAs(today)) {
minimumDateTime = now; // 最小可选时间就是现在
}
} else {
// 如果是选择结束时间
// 将开始时间转换为 DateTime 对象
final startDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
startTime.value.hour,
startTime.value.minute,
);
// 结束时间的最小值必须晚于开始时间
minimumDateTime = startDateTime;
// 如果日期是今天,并且开始时间早于现在,那么结束时间的最小值也应该是现在
if (selectedDay.isAtSameMomentAs(today) && startDateTime.isBefore(now)) {
minimumDateTime = now;
}
}
// 初始显示时间
DateTime initialDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
initialTime.hour,
initialTime.minute,
);
// 确保初始时间不早于最小时间
if (minimumDateTime != null && initialDateTime.isBefore(minimumDateTime)) {
initialDateTime = minimumDateTime;
}
DateTime tempTime = initialDateTime;
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 pickedTempTime = TimeOfDay.fromDateTime(tempTime);
// 验证条件1不能选择过去的时间
// 这个验证只在选择【今天】的【开始时间】或【结束时间】时有意义
final selectedDay = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
);
final today = DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
if (selectedDay.isAtSameMomentAs(today) &&
tempTime.isBefore(DateTime.now())) {
showToast('不能选择过去的时间');
return; // 中断执行,不关闭弹窗
}
//结束时间必须晚于开始时间
if (isStartTime) {
// 已有的结束时间 比较
final pickedStartInMinutes =
pickedTempTime.hour * 60 + pickedTempTime.minute;
final endInMinutes =
endTime.value.hour * 60 + endTime.value.minute;
if (pickedStartInMinutes >= endInMinutes) {
// 整结束时间
startTime.value = pickedTempTime;
final newEndDateTime = tempTime.add(
const Duration(minutes: 30),
);
endTime.value = TimeOfDay.fromDateTime(newEndDateTime);
} else {
//设置开始时间
startTime.value = pickedTempTime;
}
} else {
// 如果当前正在设置结束时间,我们来和已有的开始时间比较
final pickedEndInMinutes =
pickedTempTime.hour * 60 + pickedTempTime.minute;
final startInMinutes =
startTime.value.hour * 60 + startTime.value.minute;
if (pickedEndInMinutes <= startInMinutes) {
showToast('结束时间必须晚于开始时间');
return; // 中断执行,不关闭弹窗
} else {
endTime.value = pickedTempTime;
}
}
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.time,
use24hFormat: true,
initialDateTime: initialDateTime,
minimumDate: minimumDateTime,
onDateTimeChanged: (DateTime newTime) {
tempTime = newTime;
},
),
),
],
),
),
backgroundColor: Colors.transparent,
);
}
/// 提交预约
void submitReservation() async {
if (plateNumber.isEmpty) {
showToast("请先绑定车辆");
return;
}
String ampuntStr = amountController.text.toString();
if (ampuntStr.isEmpty) {
showToast("请输入需要预约的氢量");
return;
}
if (selectedStationId.value == null || selectedStationId.value!.isEmpty) {
showToast("请先选择加氢站");
return;
}
try {
showLoading("提交中");
final selectedStation = stationOptions.firstWhere(
(s) => s.hydrogenId == selectedStationId.value,
);
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("预约成功");
} 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: Color.fromARGB(255, 246, 248, 250),
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 phone = "";
String name = "";
String leftHydrogen = "0";
String workEfficiency = "0";
String fillingWeight = "0";
String fillingTimes = "0";
String plateNumber = "";
String vin = "";
@override
void onInit() {
phone = StorageService.to.phone ?? "";
name = StorageService.to.name ?? "";
getUserBindCarInfo();
getSiteList();
super.onInit();
}
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);
getCatinfo();
getJqinfo();
}
}
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();
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;
}
}
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();
}
}