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),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}