diff --git a/ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.jpg b/ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.jpg new file mode 100644 index 0000000..0e5324b Binary files /dev/null and b/ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.jpg differ diff --git a/ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.png b/ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.png deleted file mode 100644 index 88ece8f..0000000 Binary files a/ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.png and /dev/null differ diff --git a/ln_jq_app/ios/Runner/Info.plist b/ln_jq_app/ios/Runner/Info.plist index 8a7cdf2..d378c0b 100644 --- a/ln_jq_app/ios/Runner/Info.plist +++ b/ln_jq_app/ios/Runner/Info.plist @@ -2,16 +2,10 @@ - NSLocationWhenInUseUsageDescription - 需要您的位置信息以在地图上展示 - - NSLocationAlwaysAndWhenInUseUsageDescription - 我们需要您的位置来规划路线 - - NSLocationAlwaysUsageDescription - 我们需要您的位置来规划路线 - - + + + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -32,8 +26,24 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + NSCameraUsageDescription + 需要访问您的相机以扫描二维码 + NSLocationAlwaysAndWhenInUseUsageDescription + 我们需要您的位置来规划路线 + NSLocationAlwaysUsageDescription + 我们需要您的位置来规划路线 + NSLocationWhenInUseUsageDescription + 需要您的位置信息以在地图上展示 + NSPhotoLibraryAddUsageDescription + 需要访问您的相册以选择二维码图片进行识别 + NSPhotoLibraryUsageDescription + 需要访问您的相册以选择二维码图片进行识别 + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -51,17 +61,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - - NSCameraUsageDescription - 需要访问您的相机以扫描二维码 - NSPhotoLibraryUsageDescription - 需要访问您的相册以选择二维码图片进行识别 - NSPhotoLibraryAddUsageDescription - 需要访问您的相册以选择二维码图片进行识别 - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - + uses + diff --git a/ln_jq_app/lib/common/model/station_model.dart b/ln_jq_app/lib/common/model/station_model.dart index fb7280f..1292f07 100644 --- a/ln_jq_app/lib/common/model/station_model.dart +++ b/ln_jq_app/lib/common/model/station_model.dart @@ -4,6 +4,7 @@ class StationModel { final String address; final String price; final String siteStatusName; // 例如 "维修中" + final int isSelect; // 新增字段 1是可用 0是不可用 StationModel({ required this.hydrogenId, @@ -11,6 +12,7 @@ class StationModel { required this.address, required this.price, required this.siteStatusName, + required this.isSelect, }); // 从 JSON map 创建对象的工厂构造函数 @@ -21,6 +23,7 @@ class StationModel { address: json['address'] ?? '地址未知', price: json['price']?.toString() ?? '0.00', siteStatusName: json['siteStatusName'] ?? '', + isSelect: json['isSelect'] as int? ?? 0, // 新增字段的解析,默认为 0 ); } } diff --git a/ln_jq_app/lib/main.dart b/ln_jq_app/lib/main.dart index 58c3bb2..39d60cb 100644 --- a/ln_jq_app/lib/main.dart +++ b/ln_jq_app/lib/main.dart @@ -76,8 +76,6 @@ void initHttpSet() { await StorageService.to.clearLoginInfo(); Get.offAll(() => LoginPage()); return baseModel.message; - } else { - return baseModel.message; } } on Exception catch (e) { e.printInfo(); diff --git a/ln_jq_app/lib/pages/b_page/site/controller.dart b/ln_jq_app/lib/pages/b_page/site/controller.dart index 1cf6a6d..a2294f7 100644 --- a/ln_jq_app/lib/pages/b_page/site/controller.dart +++ b/ln_jq_app/lib/pages/b_page/site/controller.dart @@ -1,20 +1,23 @@ import 'dart:async'; +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/styles/theme.dart'; import 'package:ln_jq_app/storage_service.dart'; enum ReservationStatus { - pending, // 待处理 ( addStatus: 1) - completed, // 已完成 ( addStatus: 2) - rejected, // 已拒绝 ( 3) + pending, // 待处理 ( addStatus: 0) + completed, // 完成 ( addStatus: 1) + rejected, // 拒绝 ( -1) + unadded, // 未加 ( 2) unknown, // 未知状态 } class ReservationModel { final String id; final String plateNumber; - final String amount; + String amount; final String time; final String contactPerson; final String contactPhone; @@ -22,6 +25,7 @@ class ReservationModel { final String contacts; final String phone; + final String rejectReason; final String stationName; final String startTime; final String endTime; @@ -31,6 +35,7 @@ class ReservationModel { final String stateName; final String addStatus; final String addStatusName; + bool hasEdit; ReservationModel({ required this.id, @@ -39,6 +44,7 @@ class ReservationModel { required this.time, required this.contactPerson, required this.contactPhone, + required this.hasEdit, this.status = ReservationStatus.pending, required this.contacts, required this.phone, @@ -51,32 +57,39 @@ class ReservationModel { required this.stateName, required this.addStatus, required this.addStatusName, + required this.rejectReason, }); /// 工厂构造函数,用于从JSON创建ReservationModel实例 factory ReservationModel.fromJson(Map json) { - //1完成 0待处理 2已拒绝 + // 1完成 2未加 -1拒绝 0是待加氢 ReservationStatus currentStatus; int statusFromServer = json['addStatus'] as int? ?? 0; - switch (statusFromServer) { - case 0: - currentStatus = ReservationStatus.pending; - break; - case 1: - currentStatus = ReservationStatus.completed; - break; - case 2: - currentStatus = ReservationStatus.rejected; - break; - default: - currentStatus = ReservationStatus.unknown; + int state = json['state'] as int? ?? 0; + if (state == -1) { + currentStatus = ReservationStatus.rejected; + } else { + switch (statusFromServer) { + case 0: + currentStatus = ReservationStatus.pending; + break; + case 1: + currentStatus = ReservationStatus.completed; + break; + case 2: + currentStatus = ReservationStatus.unadded; + break; + default: + currentStatus = ReservationStatus.unknown; + } } // 格式化时间显示 String startTimeStr = json['startTime']?.toString() ?? ''; String endTimeStr = json['endTime']?.toString() ?? ''; String dateStr = json['date']?.toString() ?? ''; - String timeRange = (startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty) + String timeRange = + (startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty) ? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm : '时间未定'; @@ -102,6 +115,8 @@ class ReservationModel { addStatus: statusFromServer.toString(), addStatusName: json['addStatusName']?.toString() ?? '', stateName: json['stateName']?.toString() ?? '', + rejectReason: json['rejectReason']?.toString() ?? '', + hasEdit: true, ); } } @@ -119,6 +134,8 @@ class SiteController extends GetxController with BaseControllerMixin { List reservationList = []; Timer? _refreshTimer; + final TextEditingController searchController = TextEditingController(); + @override void onInit() { super.onInit(); @@ -130,6 +147,7 @@ class SiteController extends GetxController with BaseControllerMixin { @override void onClose() { stopAutoRefresh(); + searchController.dispose(); super.onClose(); } @@ -139,7 +157,7 @@ class SiteController extends GetxController with BaseControllerMixin { // 创建一个每5分钟执行一次的周期性定时器 _refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) { - fetchReservationData(); + renderData(); }); } @@ -155,13 +173,16 @@ class SiteController extends GetxController with BaseControllerMixin { Future fetchReservationData() async { showLoading("加载中"); + final String searchText = searchController.text.trim(); + try { var response = await HttpService.to.post( "appointment/orderAddHyd/sitOrderPage", data: { 'stationName': name, // 使用从 renderData 中获取到的 name 'pageNum': 1, - 'pageSize': 30, // 暂时不考虑分页,一次获取30条 + 'pageSize': 50, // 暂时不考虑分页,一次获取30条 + 'keyword': searchText, // 加氢站名称、手机号 }, ); @@ -179,7 +200,7 @@ class SiteController extends GetxController with BaseControllerMixin { // 【核心修改】处理接口返回的列表数据 final dataMap = baseModel.data as Map; - final List listFromServer = dataMap['list'] ?? []; + final List listFromServer = dataMap['records'] ?? []; // 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象 reservationList = listFromServer.map((item) { @@ -208,38 +229,289 @@ class SiteController extends GetxController with BaseControllerMixin { /// 确认预约 Future confirmReservation(String id) async { - final item = reservationList.firstWhere((item) => item.id == id); - DialogX.to.showConfirmDialog( - title: '确认预约', - message: '确定要确认车牌号${item.plateNumber}的预约吗', - onConfirm: () { - upDataService(id, 1, item); - }, - onCancel: () {}, + final item = reservationList.firstWhere( + (item) => item.id == id, + orElse: () => throw Exception('Reservation not found'), + ); + final TextEditingController amountController = TextEditingController( + text: item.hydAmount, + ); + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), // 圆角 + child: Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, // 高度自适应 + children: [ + const Text( + '确认加氢状态', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + // content 部分 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + Text( + '车牌号 ${item.plateNumber}', + style: const TextStyle( + fontSize: 16, + color: AppTheme.themeColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '加氢量', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(width: 16), + SizedBox( + width: 100, + child: TextField( + controller: amountController, + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Get.theme.primaryColor, + ), + decoration: const InputDecoration( + suffixText: 'kg', + suffixStyle: TextStyle(fontSize: 16, color: Colors.grey), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 2), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + const Text( + '请选择本次加氢的实际状态\n用于更新预约记录。', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + ), + const SizedBox(height: 24), + // actions 部分 (按钮) + Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), + child: Column( + children: [ + ElevatedButton( + onPressed: () { + Get.back(); // 关闭弹窗 + final num addHydAmount = num.tryParse(amountController.text) ?? 0; + upDataService(id, 0, 1, addHydAmount, "", item); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text('加氢完成', style: TextStyle(fontSize: 16)), + ), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () { + Get.back(); // 关闭弹窗 + upDataService(id, 0, 2, 0, "", item); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text('未加氢', style: TextStyle(fontSize: 16)), + ), + const SizedBox(height: 12), + TextButton( + onPressed: () => Get.back(), // 只关闭弹窗 + child: const Text( + '暂不处理', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ), + ], + ), + ), + ], + ), + ), + ), + barrierDismissible: false, // 点击外部不关闭弹窗 ); } - /// 拒绝预约 Future rejectReservation(String id) async { final item = reservationList.firstWhere((item) => item.id == id); - DialogX.to.showConfirmDialog( - title: '拒绝预约', - message: '确定要拒绝车牌号${item.plateNumber}的预约吗', - onConfirm: () { - upDataService(id, 2, item); - }, - onCancel: () {}, + final TextEditingController reasonController = TextEditingController(); + final RxString selectedReason = ''.obs; + + // 预设的拒绝原因列表 + final List presetReasons = ['车辆未到场', '司机联系不上', '设备故障无法加氢']; + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + '拒绝预约', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + '正在处理车牌号 ${item.plateNumber} 的预约', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, color: AppTheme.themeColor), + ), + const SizedBox(height: 24), + + // 4. 预设原因选择区域 + const Text('选择或填写拒绝原因:', style: TextStyle(color: Colors.grey)), + const SizedBox(height: 8), + Obx( + () => Wrap( + // 使用 Wrap 自动换行 + spacing: 8.0, // 水平间距 + children: presetReasons.map((reason) { + final isSelected = selectedReason.value == reason; + return ChoiceChip( + label: Text(reason), + selected: isSelected, + onSelected: (selected) { + if (selected) { + selectedReason.value = reason; + reasonController.clear(); // 选择预设原因时,清空自定义输入 + } + }, + selectedColor: Get.theme.primaryColor.withOpacity(0.2), + labelStyle: TextStyle( + color: isSelected ? Get.theme.primaryColor : Colors.black, + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + + // 5. 自定义原因输入框 + TextField( + controller: reasonController, + maxLines: 2, + decoration: InputDecoration( + hintText: '输入其它原因', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + onChanged: (text) { + if (text.isNotEmpty) { + selectedReason.value = ''; // 输入自定义原因时,取消预设选择 + } + }, + ), + const SizedBox(height: 24), + + // 6. 按钮区域 + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + // 获取最终的拒绝原因 + String finalReason = reasonController.text.trim(); + if (finalReason.isEmpty) { + finalReason = selectedReason.value; + } + + if (finalReason.isEmpty) { + showToast('请选择或填写一个拒绝原因'); + return; + } + + Get.back(); // 关闭弹窗 + upDataService(id, 1, -1, 0, finalReason, item); + }, + child: const Text('确认拒绝', style: TextStyle(color: Colors.red)), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextButton( + onPressed: () => Get.back(), + child: const Text('暂不处理', style: TextStyle(color: Colors.grey)), + ), + ), + ], + ), + ], + ), + ), + ), + ), + barrierDismissible: false, ); } - void upDataService(String id, int status, ReservationModel item) async { + //addStatus 1完成 2未加 -1拒绝 + void upDataService( + String id, + int status, + int addStatus, + num addHydAmount, + String rejectReason, + ReservationModel item, + ) async { showLoading("确认中"); try { - var responseData = await HttpService.to.post( - 'appointment/orderAddHyd/completeOrder', - data: {'id': id, 'addStatus': status}, - ); + var responseData; + if (addStatus == -1) { + responseData = await HttpService.to.post( + 'appointment/orderAddHyd/rejectOrder', + data: { + 'id': id, + 'state': -1, //拒绝使用 + "rejectReason": rejectReason, + }, + ); + } else { + responseData = await HttpService.to.post( + 'appointment/orderAddHyd/completeOrder', + data: { + 'id': id, + 'addStatus': addStatus, //完成使用 完成1,未加2 + "addHydAmount": addHydAmount, + }, + ); + } if (responseData == null && responseData!.data == null) { dismissLoading(); @@ -252,12 +524,16 @@ class SiteController extends GetxController with BaseControllerMixin { } dismissLoading(); - if (status == 1) { + //1完成 2未加 -1拒绝 + if (addStatus == 1) { item.status = ReservationStatus.completed; - } else if (status == 2) { + item.amount = "${addHydAmount}kg"; + } else if (addStatus == -1) { item.status = ReservationStatus.rejected; + } else if (addStatus == 2) { + item.status = ReservationStatus.unadded; } - updateUi(); + renderData(); } catch (e) { dismissLoading(); } @@ -267,6 +543,8 @@ class SiteController extends GetxController with BaseControllerMixin { String orderAmount = ""; String completedAmount = ""; String name = ""; + String orderTotalAmount = ""; + String orderUnfinishedAmount = ""; Future renderData() async { try { @@ -286,14 +564,22 @@ class SiteController extends GetxController with BaseControllerMixin { orderAmount = result.data["orderAmount"].toString(); completedAmount = result.data["completedAmount"].toString(); name = result.data["name"].toString(); + orderTotalAmount = result.data["orderTotalAmount"].toString(); + orderUnfinishedAmount = result.data["orderUnfinishedAmount"].toString(); leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString(); - - //加载列表数据 - fetchReservationData(); + orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString(); + orderUnfinishedAmount = orderUnfinishedAmount.isEmpty + ? "统计中" + : orderUnfinishedAmount.toString(); } catch (e) { showToast('数据异常'); } - } catch (e) {} + } catch (e) { + } finally { + + //加载列表数据 + fetchReservationData(); + } } } diff --git a/ln_jq_app/lib/pages/b_page/site/view.dart b/ln_jq_app/lib/pages/b_page/site/view.dart index 8564190..abdc2dc 100644 --- a/ln_jq_app/lib/pages/b_page/site/view.dart +++ b/ln_jq_app/lib/pages/b_page/site/view.dart @@ -94,6 +94,17 @@ class SitePage extends GetView { ], ), ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem(controller.orderTotalAmount, '加氢总量'), + _buildStatItem(controller.orderUnfinishedAmount, '未加氢总量'), + ], + ), + ), ], ), ), @@ -134,7 +145,7 @@ class SitePage extends GetView { ), ElevatedButton.icon( onPressed: () { - controller.fetchReservationData(); + controller.renderData(); }, icon: const Icon(Icons.refresh, size: 16), label: const Text('刷新'), @@ -150,6 +161,7 @@ class SitePage extends GetView { ], ), ), + _buildSearchView(), controller.hasReservationData ? _buildReservationListView() : _buildEmptyReservationView(), @@ -179,6 +191,66 @@ class SitePage extends GetView { ); } + //搜索输入框,提示可以输入车牌或者手机 + Widget _buildSearchView() { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 44, + child: TextField( + controller: controller.searchController, // 绑定控制器 + decoration: InputDecoration( + hintText: '输入车牌号或完整手机号查询', + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide(color: Get.theme.primaryColor, width: 1.5), + ), + // 清除按钮 + suffixIcon: IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + controller.searchController.clear(); + controller.fetchReservationData(); // 清除后也刷新一次 + }, + ), + ), + onSubmitted: (value) { + // 用户在键盘上点击“完成”或“搜索”时触发 + controller.fetchReservationData(); + }, + ), + ), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + // 点击“搜索”按钮时触发 + FocusScope.of(Get.context!).unfocus(); // 收起键盘 + controller.fetchReservationData(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(8), + ), + child: const Icon(Icons.search_rounded), + ), + ], + ), + ); + } + /// 构建单个统计项 Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) { return Expanded( @@ -336,7 +408,7 @@ class SitePage extends GetView { child: ElevatedButton( onPressed: () => controller.confirmReservation(item.id), style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), - child: const Text('确定'), + child: const Text('确认'), ), ), const SizedBox(width: 16), @@ -361,15 +433,19 @@ class SitePage extends GetView { Color color; switch (status) { case ReservationStatus.pending: - text = '待处理'; + text = '待加氢'; color = Colors.orange; break; case ReservationStatus.completed: - text = '已完成'; + text = '已加氢'; color = Colors.greenAccent; break; case ReservationStatus.rejected: - text = '已拒绝'; + text = '拒绝加氢'; + color = Colors.red; + break; + case ReservationStatus.unadded: + text = '未加氢'; color = Colors.red; break; default: diff --git a/ln_jq_app/lib/pages/c_page/car_info/controller.dart b/ln_jq_app/lib/pages/c_page/car_info/controller.dart index 871c06a..ac95c15 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/controller.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/controller.dart @@ -32,7 +32,7 @@ class CarInfoController extends GetxController with BaseControllerMixin { void onReady() { super.onReady(); // 如果未绑定车辆,且本次会话尚未提示过,则弹出提示 - if (!StorageService.to.hasShownBindVehicleDialog) { + if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) { Future.delayed(const Duration(milliseconds: 500), () { DialogX.to.showConfirmDialog( title: '当前尚未绑定车辆', diff --git a/ln_jq_app/lib/pages/c_page/reservation/controller.dart b/ln_jq_app/lib/pages/c_page/reservation/controller.dart index 73570e7..fc95862 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -9,36 +11,86 @@ 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 Rx selectedDate = DateTime.now().obs; - final Rx startTime = TimeOfDay.now().obs; - final Rx endTime = TimeOfDay.fromDateTime( - DateTime.now().add(const Duration(minutes: 30)), - ).obs; + 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 selectedDate = DateTime(_now.year, _now.month, _now.day).obs; + late final Rx startTime = _initialStartTime.obs; + late final Rx 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 stationOptions = [].obs; - final Rxn selectedStationId = Rxn(); // 用 ID 来选择 + final Rxn selectedStationId = Rxn(); String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value); - String get formattedStartTime => _formatTimeOfDay(startTime.value); - - String get formattedEndTime => _formatTimeOfDay(endTime.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, @@ -66,8 +118,21 @@ class C_ReservationController extends GetxController with BaseControllerMixin { ), CupertinoButton( onPressed: () { + final bool isChangingToToday = + tempDate.isAtSameMomentAs(today) && + !selectedDate.value.isAtSameMomentAs(today); + final bool isDateChanged = !tempDate.isAtSameMomentAs( + selectedDate.value, + ); + + // 更新选中的日期 selectedDate.value = tempDate; - Get.back(); + Get.back(); // 先关闭弹窗 + + // 如果日期发生了变化,则重置时间 + if (isDateChanged) { + resetTimeForSelectedDate(); + } }, child: const Text( '确认', @@ -84,28 +149,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin { 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)), + initialDateTime: selectedDate.value, + minimumDate: today, + // 最小可选日期为今天 + maximumDate: tomorrow, + // 最大可选日期为明天 + // --------------------- onDateTimeChanged: (DateTime newDate) { tempDate = newDate; }, @@ -118,52 +167,90 @@ class C_ReservationController extends GetxController with BaseControllerMixin { ); } - void pickTime(BuildContext context, bool isStartTime) { - // 1. 确定当前操作的时间和初始值 - TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value; - DateTime now = DateTime.now(); - - // 2. 准备小时和分钟的数据源 - List hours = List.generate(24, (index) => index); - List minutes = [0, 30]; - - // 3. 计算初始选中的索引 - int initialHour = initialTime.hour; - // 将初始分钟校准到0或30,并找到对应的索引 - int initialMinute = initialTime.minute; - int minuteIndex = initialMinute < 30 ? 0 : 1; - initialMinute = minutes[minuteIndex]; // 校准后的分钟 - - // 如果校准后导致时间早于当前时间,需要向上调整 - final selectedDay = DateTime(selectedDate.value.year, selectedDate.value.month, selectedDate.value.day); + void resetTimeForSelectedDate() { + final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); - if (selectedDay.isAtSameMomentAs(today)) { - if (initialHour < now.hour || (initialHour == now.hour && initialMinute < now.minute)) { - initialHour = now.hour; - if (now.minute > 30) { - // 如果当前分钟>30, 则进位到下一小时的0分 - initialHour = (now.hour + 1) % 24; - initialMinute = 0; - } else { - // 否则,取30分 - initialMinute = 30; + + // 判断新选择的日期是不是今天 + 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 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)); } } } - // 重新获取校准后的索引 - minuteIndex = minutes.indexOf(initialMinute); + 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; + } - // 4. 创建 FixedExtentScrollController 来控制滚轮的初始位置 - final FixedExtentScrollController hourController = - FixedExtentScrollController(initialItem: hours.indexOf(initialHour)); - final FixedExtentScrollController minuteController = - FixedExtentScrollController(initialItem: minuteIndex); + TimeSlot tempSlot = availableSlots[initialItem]; - // 5. 存储临时选择的值 - int tempHour = initialHour; - int tempMinute = initialMinute; + final FixedExtentScrollController scrollController = FixedExtentScrollController( + initialItem: initialItem, + ); Get.bottomSheet( Container( @@ -184,55 +271,23 @@ class C_ReservationController extends GetxController with BaseControllerMixin { children: [ CupertinoButton( onPressed: () => Get.back(), - child: const Text('取消', style: TextStyle(color: CupertinoColors.systemGrey)), + child: const Text( + '取消', + style: TextStyle(color: CupertinoColors.systemGrey), + ), ), CupertinoButton( onPressed: () { - final pickedTempTime = TimeOfDay(hour: tempHour, minute: tempMinute); - final now = DateTime.now(); - - // --- 合并和简化校验逻辑 --- - final selectedDateTime = DateTime( - selectedDate.value.year, - selectedDate.value.month, - selectedDate.value.day, - pickedTempTime.hour, - pickedTempTime.minute, - ); - - // 验证1: 不能选择过去的时间(留出一分钟缓冲) - if (selectedDateTime.isBefore(now.subtract(const Duration(minutes: 1)))) { - showToast('不能选择过去的时间'); - return; - } - - // 验证2: 结束时间必须晚于开始时间 - if (isStartTime) { - startTime.value = pickedTempTime; - final startInMinutes = pickedTempTime.hour * 60 + pickedTempTime.minute; - final endInMinutes = endTime.value.hour * 60 + endTime.value.minute; - - // 如果新的开始时间大于等于结束时间,自动将结束时间设置为开始时间+30分钟 - if (startInMinutes >= endInMinutes) { - final newEndDateTime = selectedDateTime.add(const Duration(minutes: 30)); - endTime.value = TimeOfDay.fromDateTime(newEndDateTime); - } - } else { // 正在设置结束时间 - final startInMinutes = startTime.value.hour * 60 + startTime.value.minute; - final endInMinutes = pickedTempTime.hour * 60 + pickedTempTime.minute; - - if (endInMinutes <= startInMinutes) { - showToast('结束时间必须晚于开始时间'); - return; - } - endTime.value = pickedTempTime; - } - + startTime.value = tempSlot.start; + endTime.value = tempSlot.end; Get.back(); }, child: const Text( '确认', - style: TextStyle(color: AppTheme.themeColor, fontWeight: FontWeight.bold), + style: TextStyle( + color: AppTheme.themeColor, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -240,36 +295,15 @@ class C_ReservationController extends GetxController with BaseControllerMixin { ), const Divider(height: 1, color: Color(0xFFE5E5E5)), Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 小时选择器 - Expanded( - child: CupertinoPicker( - scrollController: hourController, - itemExtent: 32.0, - onSelectedItemChanged: (index) { - tempHour = hours[index]; - }, - children: hours - .map((h) => Center(child: Text(h.toString().padLeft(2, '0')))) - .toList(), - ), - ), - // 分钟选择器 - Expanded( - child: CupertinoPicker( - scrollController: minuteController, - itemExtent: 32.0, - onSelectedItemChanged: (index) { - tempMinute = minutes[index]; - }, - children: minutes - .map((m) => Center(child: Text(m.toString().padLeft(2, '0')))) - .toList(), - ), - ), - ], + child: CupertinoPicker( + scrollController: scrollController, + itemExtent: 40.0, + onSelectedItemChanged: (index) { + tempSlot = availableSlots[index]; + }, + children: availableSlots + .map((slot) => Center(child: Text(slot.display))) + .toList(), ), ), ], @@ -279,8 +313,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin { ); } - - // 用于存储上一次成功预约的信息 ReservationModel? lastSuccessfulReservation; @@ -311,37 +343,46 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } final dateStr = formattedDate; - final startTimeStr = '$dateStr ${formattedStartTime}:00'; + final startTimeStr = + '$dateStr ${_formatTimeOfDay(startTime.value)}:00'; // Use helper directly + if (lastSuccessfulReservation != null && lastSuccessfulReservation!.id == selectedStationId.value && lastSuccessfulReservation!.startTime == startTimeStr) { - showToast("请勿重复提交相同的预约"); + showToast("请勿重复提交相同时间段的预约,可在“查看预约”中修改"); return; } - // 将选择的日期和时间组合成一个完整的 DateTime 对象 - final reservationStartDateTime = DateTime( + final reservationEndDateTime = DateTime( selectedDate.value.year, selectedDate.value.month, selectedDate.value.day, - startTime.value.hour, - startTime.value.minute, + endTime.value.hour, + endTime.value.minute, ); - // 检查预约时间是否在当前时间之前 - if (reservationStartDateTime.isBefore(DateTime.now())) { - showToast("不可预约过去的时间"); + //判断预约区间的结束时间是否早于当前时间(留出1分钟缓冲) + if (reservationEndDateTime.isBefore( + DateTime.now().subtract(const Duration(minutes: 1)), + )) { + 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'; + 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', @@ -369,7 +410,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin { if (result.code == 0) { showSuccessToast("预约成功"); - // 预约成功后,保存当前预约信息 lastSuccessfulReservation = ReservationModel( id: selectedStationId.value!, hydAmount: ampuntStr, @@ -388,14 +428,16 @@ class C_ReservationController extends GetxController with BaseControllerMixin { stateName: '', addStatus: '', addStatusName: '', + rejectReason: '', + hasEdit: true, ); //打开预约列表 - Future.delayed(const Duration(milliseconds: 800), () { + Future.delayed(const Duration(milliseconds: 500), () { getReservationList(); }); } else { - showErrorToast(result.message); + showToast(result.error); } } catch (e) { dismissLoading(); @@ -409,8 +451,16 @@ class C_ReservationController extends GetxController with BaseControllerMixin { // 新增预约数据列表 List reservationList = []; + // --- 用于防抖的 Timer --- + Timer? _debounce; + //查看预约列表 void getReservationList() async { + if (_debounce?.isActive ?? false) { + return; + } + _debounce = Timer(const Duration(seconds: 1), () {}); + showLoading("加载中"); try { @@ -434,13 +484,32 @@ class C_ReservationController extends GetxController with BaseControllerMixin { if (baseModel.code == 0 && baseModel.data != null) { final dataMap = baseModel.data as Map; - final List listFromServer = dataMap['list'] ?? []; + final List listFromServer = dataMap['records'] ?? []; reservationList = listFromServer.map((item) { return ReservationModel.fromJson(item as Map); }).toList(); // 根据列表是否为空来更新 hasReservationData 状态 hasReservationData = reservationList.isNotEmpty; + + 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; + } + } } else { showToast(baseModel.message); hasReservationData = false; @@ -537,33 +606,56 @@ class C_ReservationController extends GetxController with BaseControllerMixin { ), // Blue border ), child: Text( - reservation.stateName, + reservation.stateName + + "-" + + reservation.addStatusName, 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, - ), - ), - ), + !reservation.hasEdit + ? SizedBox() + : GestureDetector( + onTap: () async { + var result = await Get.to( + () => ReservationEditPage(), + arguments: { + 'reservation': reservation, + 'difference': difference, + }, + binding: BindingsBuilder(() { + Get.put(ReservationEditController()); + }), + preventDuplicates: false, + ); + if (result == true) { + Get.back(); + getReservationList(); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color( + 0xFFFFF7E6, + ), // Light orange background + borderRadius: BorderRadius.circular(12), + ), + child: Text( + "修改", + style: const TextStyle( + color: Color(0xFFFA8C16), + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), ], ), const SizedBox(height: 12), @@ -575,6 +667,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin { _buildDetailRow('结束时间:', reservation.endTime), _buildDetailRow('联系人:', reservation.contacts), _buildDetailRow('联系电话:', reservation.phone), + reservation.addStatus == "5" + ? _buildDetailRow('拒绝原因:', reservation.rejectReason) + : SizedBox(), ], ), ), @@ -764,7 +859,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { HttpService.to.dio.options.headers = originalHeaders; // 如果未绑定车辆,且本次会话尚未提示过,则弹出提示 - if (!StorageService.to.hasShownBindVehicleDialog) { + if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) { Future.delayed(const Duration(milliseconds: 500), () { DialogX.to.showConfirmDialog( title: '当前尚未绑定车辆', @@ -791,6 +886,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin { void onClose() { amountController.dispose(); plateNumberController.dispose(); + if (_debounce != null) { + _debounce?.cancel(); + } super.onClose(); } } diff --git a/ln_jq_app/lib/pages/c_page/reservation/view.dart b/ln_jq_app/lib/pages/c_page/reservation/view.dart index 7b8bfb9..787617a 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/view.dart @@ -258,16 +258,10 @@ class ReservationPage extends GetView { onTap: () => controller.pickDate(context), ), _buildPickerRow( - label: '开始时间', - value: controller.formattedStartTime, + label: '预约时间', + value: controller.formattedTimeSlot, icon: Icons.access_time_outlined, - onTap: () => controller.pickTime(context, true), - ), - _buildPickerRow( - label: '结束时间', - value: controller.formattedEndTime, - icon: Icons.access_time_outlined, - onTap: () => controller.pickTime(context, false), + onTap: () => controller.pickTime(context), ), _buildTextField( label: '预约氢量(KG)', @@ -438,6 +432,7 @@ class ReservationPage extends GetView { .map( (station) => DropdownMenuItem( value: station.hydrogenId, // value 是站点的唯一ID + enabled: station.isSelect == 1, child: _buildDropdownItem(station), // child 是自定义的 Widget ), ) @@ -484,7 +479,10 @@ class ReservationPage extends GetView { /// 构建下拉菜单中的每一项 Widget _buildDropdownItem(StationModel station) { - bool isMaintenance = (station.siteStatusName != '营运中'); + bool isSelectable = (station.isSelect == 1); + //营运并且可用 + bool isMaintenance = ((station.siteStatusName != '营运中') && isSelectable); + final textColor = isSelectable ? Colors.black : Colors.grey; return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( @@ -501,14 +499,22 @@ class ReservationPage extends GetView { Flexible( child: Text( station.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: TextStyle( + color: textColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), Text( ' | ¥${station.price}/kg', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: TextStyle( + color: textColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), if (isMaintenance) const SizedBox(width: 8), if (isMaintenance) diff --git a/ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart b/ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart new file mode 100644 index 0000000..1451b7a --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart @@ -0,0 +1,253 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.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/pages/b_page/site/controller.dart'; // For ReservationModel +import 'package:ln_jq_app/common/styles/theme.dart'; + +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 ReservationEditController extends GetxController with BaseControllerMixin { + late final ReservationModel reservation; + late final String difference; + + // --- State Variables --- + final Rx selectedDate = DateTime.now().obs; + final Rx startTime = TimeOfDay.now().obs; + final Rx endTime = TimeOfDay.now().obs; + final TextEditingController amountController = TextEditingController(); + + // --- Getters for UI display --- + String get formattedTimeSlot => + '${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}'; + + @override + void onInit() { + super.onInit(); + // Expect a Map containing both the reservation and the difference + final args = Get.arguments as Map; + reservation = args['reservation'] as ReservationModel; + difference = args['difference'] as String? ?? '0'; + + try { + // Initialize the UI with the data from the passed reservation object + selectedDate.value = DateFormat('yyyy-MM-dd').parse(reservation.date); + + final startDateTime = DateFormat( + 'yyyy-MM-dd HH:mm:ss', + ).parse(reservation.startTime); + startTime.value = TimeOfDay.fromDateTime(startDateTime); + + final endDateTime = DateFormat('yyyy-MM-dd HH:mm:ss').parse(reservation.endTime); + endTime.value = TimeOfDay.fromDateTime(endDateTime); + + amountController.text = reservation.hydAmount.replaceAll('kg', ''); + } catch (e) { + showErrorToast("加载预约数据时出错"); + Get.back(); // Go back if data is invalid + } + } + + @override + void onClose() { + amountController.dispose(); + super.onClose(); + } + + /// Reusable time slot picker logic, copied from the reservation creation page. + 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 availableSlots = []; + for (int i = 0; i < 48; i++) { + final startMinutes = i * 30; + final endMinutes = startMinutes + 30; + final slotStartTime = TimeOfDay( + hour: startMinutes ~/ 60, + minute: startMinutes % 60, + ); + final slotEndTime = TimeOfDay( + hour: (endMinutes ~/ 60) % 24, + minute: endMinutes % 60, + ); + + final slotStartDateTime = DateTime( + selectedDate.value.year, + selectedDate.value.month, + selectedDate.value.day, + slotStartTime.hour, + slotStartTime.minute, + ); + + if (!isToday || slotStartDateTime.isAfter(now)) { + availableSlots.add(TimeSlot(slotStartTime, slotEndTime)); + } + } + + 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]; + + 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: () { + if (Get.isBottomSheetOpen ?? false) { + Navigator.of(Get.overlayContext!).pop(); // 更加安全的关闭当前弹窗的方式 + } + }, + child: const Text( + '取消', + style: TextStyle(color: CupertinoColors.systemGrey), + ), + ), + CupertinoButton( + onPressed: () { + startTime.value = tempSlot.start; + endTime.value = tempSlot.end; + if (Get.isBottomSheetOpen ?? false) { + Navigator.of(Get.overlayContext!).pop(); // 更加安全的关闭当前弹窗的方式 + } + }, + child: const Text( + '确认', + style: TextStyle( + color: AppTheme.themeColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + const Divider(height: 1, color: Color(0xFFE5E5E5)), + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController(initialItem: initialItem), + itemExtent: 40.0, + onSelectedItemChanged: (index) { + tempSlot = availableSlots[index]; + }, + children: availableSlots + .map((slot) => Center(child: Text(slot.display))) + .toList(), + ), + ), + ], + ), + ), + backgroundColor: Colors.transparent, + ); + } + + /// This function will be called when the 'Save Changes' button is pressed. + void updateReservation() async { + String amountStr = amountController.text.toString(); + if (amountStr.isEmpty) { + showToast("请输入需要预约的氢量"); + return; + } + double amountDouble = (double.tryParse(amountStr) ?? 0.0); + if (amountDouble <= 0) { + showToast("预约氢量必须大于0"); + return; + } + if (amountDouble > (double.tryParse(difference) ?? 0.0)) { + showToast('当前最大可预约氢量为$difference(KG)'); + return; + } + + showLoading("正在保存修改..."); + + final dateStr = DateFormat('yyyy-MM-dd').format(selectedDate.value); + final startTimeStr = '$dateStr ${_formatTimeOfDay(startTime.value)}:00'; + final endTimeStr = '$dateStr ${_formatTimeOfDay(endTime.value)}:00'; + + try { + var responseData = await HttpService.to.post( + 'appointment/orderAddHyd/saveOrUpdate', + data: { + 'id': reservation.id, + 'startTime': startTimeStr, + 'endTime': endTimeStr, + 'hydAmount': amountStr, + }, + ); + + if (responseData == null || responseData.data == null) { + showToast('服务暂不可用,请稍后'); + dismissLoading(); + return; + } + + var result = BaseModel.fromJson(responseData.data); + + if (result.code == 0) { + showSuccessToast("修改成功"); + //弹窗刷新数据 + Get.back(result: true); + } else { + showToast(result.error); + } + } catch (e) { + showErrorToast("保存失败,请稍后再试"); + } finally { + dismissLoading(); + } + } + + String _formatTimeOfDay(TimeOfDay time) { + final hour = time.hour.toString().padLeft(2, '0'); + final minute = time.minute.toString().padLeft(2, '0'); + return '$hour:$minute'; + } + + @override + String get builderId => "reservationeditpage"; +} diff --git a/ln_jq_app/lib/pages/c_page/reservation_edit/view.dart b/ln_jq_app/lib/pages/c_page/reservation_edit/view.dart new file mode 100644 index 0000000..47332e2 --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/reservation_edit/view.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'controller.dart'; + +class ReservationEditPage extends GetView { + const ReservationEditPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: ReservationEditController(), + id: 'reservationeditpage', + builder: (_) { + return Scaffold( + appBar: AppBar(title: const Text('修改预约'), centerTitle: true), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPickerRow( + label: '预约时间', + value: controller.formattedTimeSlot, + icon: Icons.access_time_outlined, + onTap: () => controller.pickTime(context), + ), + _buildTextField( + label: '预约氢量(KG)', + controller: controller.amountController, + // Use Obx to make the hint text responsive if needed, though here it's static. + hint: '当前最大可预约氢量${controller.difference}(KG)', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: controller.updateReservation, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: const Text( + '保存修改', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildPickerRow({ + required String label, + required String value, + required IconData icon, + required VoidCallback onTap, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), + const SizedBox(height: 8), + InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[400]!), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(value, style: const TextStyle(fontSize: 16)), + Icon(icon, color: Colors.grey, size: 20), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildTextField({ + required String label, + required TextEditingController controller, + required String hint, + TextInputType? keyboardType, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), + const SizedBox(height: 8), + TextFormField( + controller: controller, + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hint, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), + ), + ), + ], + ), + ); + } +}