Merge branch 'dev'

司机端-加氢预约  预约时间段修改  1、开始结束时间整合成一个  2、时间只开放当日和次日
司机端-加氢预约  提交预约不可提交限制 1、非营业站点 2、预约时间不可重复
司机端-预约列表 1、新增到站时间、氢量修改 2、显示拒绝加氢原因
加氢站-加氢预约  1、新增当日预约加车牌/手机号筛选  2、新增加氢总量 未加氢量  3、 确认拒绝流程优化:完成可填写具体加氢量,新增拒绝原因
# Conflicts:
#	ln_jq_app/lib/common/styles/theme.dart
This commit is contained in:
2025-12-11 15:48:10 +08:00
12 changed files with 1121 additions and 274 deletions

View File

@@ -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
);
}
}

View File

@@ -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();

View File

@@ -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<String, dynamic> 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<ReservationModel> 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<void> 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<String, dynamic>;
final List<dynamic> listFromServer = dataMap['list'] ?? [];
final List<dynamic> listFromServer = dataMap['records'] ?? [];
// 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象
reservationList = listFromServer.map((item) {
@@ -208,38 +229,289 @@ class SiteController extends GetxController with BaseControllerMixin {
/// 确认预约
Future<void> 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<void> 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<String> 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<void> 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();
}
}
}

View File

@@ -94,6 +94,17 @@ class SitePage extends GetView<SiteController> {
],
),
),
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<SiteController> {
),
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<SiteController> {
],
),
),
_buildSearchView(),
controller.hasReservationData
? _buildReservationListView()
: _buildEmptyReservationView(),
@@ -179,6 +191,66 @@ class SitePage extends GetView<SiteController> {
);
}
//搜索输入框,提示可以输入车牌或者手机
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<SiteController> {
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<SiteController> {
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:

View File

@@ -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: '当前尚未绑定车辆',

View File

@@ -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<DateTime> selectedDate = DateTime.now().obs;
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
final Rx<TimeOfDay> endTime = TimeOfDay.fromDateTime(
DateTime.now().add(const Duration(minutes: 30)),
).obs;
final DateTime _now = DateTime.now();
// 计算当前时间属于哪个半小时区间
late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now);
late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 30)),
);
late final Rx<DateTime> selectedDate = DateTime(_now.year, _now.month, _now.day).obs;
late final Rx<TimeOfDay> startTime = _initialStartTime.obs;
late final Rx<TimeOfDay> endTime = _initialEndTime.obs;
/// 静态辅助方法,用于计算初始的开始时间
static TimeOfDay _calculateInitialStartTime(DateTime now) {
if (now.minute < 30) {
// 如果当前分钟小于30则开始时间为当前小时的0分
return TimeOfDay(hour: now.hour, minute: 0);
} else {
// 如果当前分钟大于等于30则开始时间为当前小时的30分
return TimeOfDay(hour: now.hour, minute: 30);
}
}
/// 静态辅助方法将TimeOfDay转换为DateTime
static DateTime _getDateTimeFromTimeOfDay(TimeOfDay time) {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day, time.hour, time.minute);
}
final TextEditingController amountController = TextEditingController();
TextEditingController plateNumberController = TextEditingController();
final RxList<StationModel> stationOptions = <StationModel>[].obs;
final Rxn<String> selectedStationId = Rxn<String>(); // 用 ID 来选择
final Rxn<String> selectedStationId = Rxn<String>();
String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value);
String get formattedStartTime => _formatTimeOfDay(startTime.value);
String get formattedEndTime => _formatTimeOfDay(endTime.value);
/// 时间段
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<int> hours = List<int>.generate(24, (index) => index);
List<int> 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<TimeSlot> availableSlots = [];
for (int i = 0; i < 48; i++) {
final startMinutes = i * 30;
final endMinutes = startMinutes + 30;
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
final slotStartDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
startTime.hour,
startTime.minute,
);
// 如果不是今天,所有时间段都有效
if (!isToday) {
availableSlots.add(TimeSlot(startTime, endTime));
} else {
// 如果是今天,需要判断该时间段是否可选
// 创建时间段的结束时间对象
final slotEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.hour,
endTime.minute,
);
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
if (slotEndDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(startTime, endTime));
}
}
}
// 重新获取校准后的索引
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<ReservationModel> 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<String, dynamic>;
final List<dynamic> listFromServer = dataMap['list'] ?? [];
final List<dynamic> listFromServer = dataMap['records'] ?? [];
reservationList = listFromServer.map((item) {
return ReservationModel.fromJson(item as Map<String, dynamic>);
}).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();
}
}

View File

@@ -258,16 +258,10 @@ class ReservationPage extends GetView<C_ReservationController> {
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<C_ReservationController> {
.map(
(station) => DropdownMenuItem<String>(
value: station.hydrogenId, // value 是站点的唯一ID
enabled: station.isSelect == 1,
child: _buildDropdownItem(station), // child 是自定义的 Widget
),
)
@@ -484,7 +479,10 @@ class ReservationPage extends GetView<C_ReservationController> {
/// 构建下拉菜单中的每一项
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<C_ReservationController> {
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)

View File

@@ -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<DateTime> selectedDate = DateTime.now().obs;
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
final Rx<TimeOfDay> 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<String, dynamic>;
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<TimeSlot> 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";
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class ReservationEditPage extends GetView<ReservationEditController> {
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),
),
),
],
),
);
}
}