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:
BIN
ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.jpg
Normal file
BIN
ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
@@ -2,16 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要您的位置信息以在地图上展示</string>
|
||||
<!-- 建议添加:即使你只申请WhenInUse,有些插件逻辑可能会检查这个key -->
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>我们需要您的位置来规划路线</string>
|
||||
<!-- 建议添加:旧版本iOS兼容 -->
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>我们需要您的位置来规划路线</string>
|
||||
|
||||
|
||||
<key></key>
|
||||
<string></string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -32,8 +26,24 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问您的相机以扫描二维码</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>我们需要您的位置来规划路线</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>我们需要您的位置来规划路线</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要您的位置信息以在地图上展示</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -51,17 +61,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问您的相机以扫描二维码</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
||||
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>uses</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,13 +57,18 @@ 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;
|
||||
int state = json['state'] as int? ?? 0;
|
||||
if (state == -1) {
|
||||
currentStatus = ReservationStatus.rejected;
|
||||
} else {
|
||||
switch (statusFromServer) {
|
||||
case 0:
|
||||
currentStatus = ReservationStatus.pending;
|
||||
@@ -66,17 +77,19 @@ class ReservationModel {
|
||||
currentStatus = ReservationStatus.completed;
|
||||
break;
|
||||
case 2:
|
||||
currentStatus = ReservationStatus.rejected;
|
||||
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);
|
||||
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);
|
||||
},
|
||||
onCancel: () {},
|
||||
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);
|
||||
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(); // 选择预设原因时,清空自定义输入
|
||||
}
|
||||
},
|
||||
onCancel: () {},
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: '当前尚未绑定车辆',
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 判断新选择的日期是不是今天
|
||||
if (selectedDate.value.isAtSameMomentAs(today)) {
|
||||
// 如果是今天,就将时间重置为当前时间所在的半小时区间
|
||||
startTime.value = _calculateInitialStartTime(now);
|
||||
endTime.value = TimeOfDay.fromDateTime(
|
||||
_getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)),
|
||||
);
|
||||
} else {
|
||||
// 否则,取30分
|
||||
initialMinute = 30;
|
||||
// 如果是明天(或其他未来日期),则可以将时间重置为一天的最早可用时间,例如 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,94 +271,41 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, color: Color(0xFFE5E5E5)),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 小时选择器
|
||||
Expanded(
|
||||
child: CupertinoPicker(
|
||||
scrollController: hourController,
|
||||
itemExtent: 32.0,
|
||||
scrollController: scrollController,
|
||||
itemExtent: 40.0,
|
||||
onSelectedItemChanged: (index) {
|
||||
tempHour = hours[index];
|
||||
tempSlot = availableSlots[index];
|
||||
},
|
||||
children: hours
|
||||
.map((h) => Center(child: Text(h.toString().padLeft(2, '0'))))
|
||||
children: availableSlots
|
||||
.map((slot) => Center(child: Text(slot.display)))
|
||||
.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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -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,14 +606,36 @@ 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(
|
||||
!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,
|
||||
@@ -556,11 +647,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
reservation.addStatusName,
|
||||
"修改",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFA8C16),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
253
ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart
Normal file
253
ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart
Normal 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";
|
||||
}
|
||||
127
ln_jq_app/lib/pages/c_page/reservation_edit/view.dart
Normal file
127
ln_jq_app/lib/pages/c_page/reservation_edit/view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user