Compare commits
8 Commits
ec1c554eb3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 88b16ca69e | |||
| ba3467810c | |||
| 0bfedd54cb | |||
| f25d7e4567 | |||
| 4cdedff654 | |||
| 4f99ab4164 | |||
| cdc5af7f45 | |||
| 20e5e58ded |
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ class AppTheme {
|
||||
|
||||
static const Color themeColor = Color(0xFF0c83c3);
|
||||
|
||||
|
||||
static const String test_service_url = "http://47.100.49.118:8090/api/";
|
||||
//http://192.168.110.222:8080/
|
||||
static const String test_service_url = "https://beta-esg.api.lnh2e.com/";
|
||||
static const String release_service_url = "";
|
||||
|
||||
//加氢站相关查询
|
||||
static const String jiaqing_service_url = "https://lnh2e.com/api/lingniu-manager-v1/v1/";
|
||||
static const String jiaqing_service_url =
|
||||
"https://beta.lnh2e.com/api/lingniu-manager-v1/v1/";
|
||||
|
||||
//车辆信息
|
||||
static const String car_service_url = "http://47.99.166.38:20000/";
|
||||
|
||||
@@ -56,10 +59,7 @@ class AppTheme {
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color.fromARGB(255, 34, 34, 34),
|
||||
centerTitle: true,
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
bottomAppBarTheme: BottomAppBarThemeData(
|
||||
color: Color.fromARGB(255, 34, 34, 34),
|
||||
|
||||
@@ -30,7 +30,7 @@ void main() async {
|
||||
// 设计稿尺寸 单位:dp
|
||||
designSize: const Size(390, 844),
|
||||
// Getx Log
|
||||
enableLog: false,
|
||||
enableLog: true,
|
||||
// 默认的跳转动画
|
||||
defaultTransition: Transition.rightToLeft,
|
||||
// 主题模式
|
||||
|
||||
@@ -76,7 +76,8 @@ class ReservationModel {
|
||||
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
|
||||
: '时间未定';
|
||||
|
||||
@@ -119,6 +120,8 @@ class SiteController extends GetxController with BaseControllerMixin {
|
||||
List<ReservationModel> reservationList = [];
|
||||
Timer? _refreshTimer;
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@@ -130,6 +133,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
||||
@override
|
||||
void onClose() {
|
||||
stopAutoRefresh();
|
||||
searchController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@@ -155,13 +159,17 @@ 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条
|
||||
'plateNumber': searchText, // 加氢站名称
|
||||
'phone': searchText, //手机号
|
||||
},
|
||||
);
|
||||
|
||||
@@ -267,6 +275,8 @@ class SiteController extends GetxController with BaseControllerMixin {
|
||||
String orderAmount = "";
|
||||
String completedAmount = "";
|
||||
String name = "";
|
||||
String orderTotalAmount = "";
|
||||
String orderUnfinishedAmount = "";
|
||||
|
||||
Future<void> renderData() async {
|
||||
try {
|
||||
@@ -286,8 +296,14 @@ 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"] ?? "";
|
||||
orderUnfinishedAmount = result.data["orderUnfinishedAmount"] ?? "";
|
||||
|
||||
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
|
||||
orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString();
|
||||
orderUnfinishedAmount = orderUnfinishedAmount.isEmpty
|
||||
? "统计中"
|
||||
: orderUnfinishedAmount.toString();
|
||||
|
||||
//加载列表数据
|
||||
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, '未加氢总量'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -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(
|
||||
|
||||
@@ -9,32 +9,72 @@ 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;
|
||||
@@ -84,22 +124,7 @@ 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点”
|
||||
initialDateTime: selectedDate.value,
|
||||
minimumDate: DateTime(
|
||||
DateTime.now().year,
|
||||
DateTime.now().month,
|
||||
@@ -118,52 +143,72 @@ 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();
|
||||
///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;
|
||||
|
||||
// 2. 准备小时和分钟的数据源
|
||||
List<int> hours = List<int>.generate(24, (index) => index);
|
||||
List<int> minutes = [0, 30];
|
||||
final List<TimeSlot> availableSlots = [];
|
||||
for (int i = 0; i < 48; i++) {
|
||||
final startMinutes = i * 30;
|
||||
final endMinutes = startMinutes + 30;
|
||||
|
||||
// 3. 计算初始选中的索引
|
||||
int initialHour = initialTime.hour;
|
||||
// 将初始分钟校准到0或30,并找到对应的索引
|
||||
int initialMinute = initialTime.minute;
|
||||
int minuteIndex = initialMinute < 30 ? 0 : 1;
|
||||
initialMinute = minutes[minuteIndex]; // 校准后的分钟
|
||||
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
|
||||
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
|
||||
|
||||
// 如果校准后导致时间早于当前时间,需要向上调整
|
||||
final selectedDay = DateTime(selectedDate.value.year, selectedDate.value.month, selectedDate.value.day);
|
||||
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;
|
||||
final slotStartDateTime = DateTime(
|
||||
selectedDate.value.year,
|
||||
selectedDate.value.month,
|
||||
selectedDate.value.day,
|
||||
startTime.hour,
|
||||
startTime.minute,
|
||||
);
|
||||
|
||||
// 如果不是今天,所有时间段都有效
|
||||
if (!isToday) {
|
||||
availableSlots.add(TimeSlot(startTime, endTime));
|
||||
} else {
|
||||
// 否则,取30分
|
||||
initialMinute = 30;
|
||||
// 如果是今天,需要判断该时间段是否可选
|
||||
// 创建时间段的结束时间对象
|
||||
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 +229,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 +271,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 用于存储上一次成功预约的信息
|
||||
ReservationModel? lastSuccessfulReservation;
|
||||
|
||||
@@ -311,7 +301,9 @@ 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) {
|
||||
@@ -319,29 +311,36 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
||||
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 +368,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
||||
if (result.code == 0) {
|
||||
showSuccessToast("预约成功");
|
||||
|
||||
// 预约成功后,保存当前预约信息
|
||||
lastSuccessfulReservation = ReservationModel(
|
||||
id: selectedStationId.value!,
|
||||
hydAmount: ampuntStr,
|
||||
@@ -537,14 +535,37 @@ 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(
|
||||
plateNumber.isEmpty
|
||||
? SizedBox()
|
||||
: GestureDetector(
|
||||
onTap: () async {
|
||||
var result = await Get.to(
|
||||
() => ReservationEditPage(),
|
||||
arguments: {
|
||||
'reservation': reservation,
|
||||
'difference': difference,
|
||||
},
|
||||
binding: BindingsBuilder(() {
|
||||
Get.put(ReservationEditController());
|
||||
}),
|
||||
preventDuplicates: false,
|
||||
);
|
||||
showToast("555$result");
|
||||
if (result == true) {
|
||||
Get.back();
|
||||
getReservationList();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
@@ -556,11 +577,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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
showErrorToast(result.message);
|
||||
}
|
||||
} 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