diff --git a/ln_jq_app/lib/common/model/station_model.dart b/ln_jq_app/lib/common/model/station_model.dart new file mode 100644 index 0000000..fb7280f --- /dev/null +++ b/ln_jq_app/lib/common/model/station_model.dart @@ -0,0 +1,26 @@ +class StationModel { + final String hydrogenId; + final String name; + final String address; + final String price; + final String siteStatusName; // 例如 "维修中" + + StationModel({ + required this.hydrogenId, + required this.name, + required this.address, + required this.price, + required this.siteStatusName, + }); + + // 从 JSON map 创建对象的工厂构造函数 + factory StationModel.fromJson(Map json) { + return StationModel( + hydrogenId: json['hydrogenId'] ?? '', + name: json['name'] ?? '未知站点', + address: json['address'] ?? '地址未知', + price: json['price']?.toString() ?? '0.00', + siteStatusName: json['siteStatusName'] ?? '', + ); + } +} diff --git a/ln_jq_app/lib/pages/c_page/reservation/controller.dart b/ln_jq_app/lib/pages/c_page/reservation/controller.dart index c6130d8..1219644 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -6,6 +6,7 @@ import 'package:getx_scaffold/common/services/http.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/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/storage_service.dart'; @@ -25,8 +26,8 @@ class ReservationController extends GetxController with BaseControllerMixin { final TextEditingController amountController = TextEditingController(); TextEditingController plateNumberController = TextEditingController(); - final List stationOptions = ['诚志AP银河路加氢站', '站点B', '站点C']; - final Rx selectedStation = '诚志AP银河路加氢站'.obs; + final RxList stationOptions = [].obs; + final Rxn selectedStationId = Rxn(); // 用 ID 来选择 String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value); @@ -97,7 +98,7 @@ class ReservationController extends GetxController with BaseControllerMixin { ) : selectedDate.value, - // 【核心】设置最小可选日期为“今天凌晨0点” + // 设置最小可选日期为“今天凌晨0点” minimumDate: DateTime( DateTime.now().year, DateTime.now().month, @@ -117,11 +118,11 @@ class ReservationController extends GetxController with BaseControllerMixin { } void pickTime(BuildContext context, bool isStartTime) { - // 1. 确定当前操作的时间和初始值 + // 确定当前操作的时间和初始值 TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value; DateTime now = DateTime.now(); - // 2.【核心】计算最小可选时间 (Minimum Date) + // 计算最小可选时间 DateTime? minimumDateTime; // 默认为 null,即可选任意时间 // 获取当前选择的日期(年月日) @@ -135,12 +136,12 @@ class ReservationController extends GetxController with BaseControllerMixin { final today = DateTime(now.year, now.month, now.day); if (isStartTime) { - // 如果是选择【开始时间】并且日期是【今天】 + // 如果是选择开始时间并且日期是 今天 if (selectedDay.isAtSameMomentAs(today)) { minimumDateTime = now; // 最小可选时间就是现在 } } else { - // 如果是选择【结束时间】 + // 如果是选择结束时间 // 将开始时间转换为 DateTime 对象 final startDateTime = DateTime( selectedDate.value.year, @@ -159,7 +160,7 @@ class ReservationController extends GetxController with BaseControllerMixin { } } - // 3. 准备 DatePicker 的初始显示时间 + // 初始显示时间 DateTime initialDateTime = DateTime( selectedDate.value.year, selectedDate.value.month, @@ -225,25 +226,25 @@ class ReservationController extends GetxController with BaseControllerMixin { //结束时间必须晚于开始时间 if (isStartTime) { - // 和【已有的结束时间】比较 + // 已有的结束时间 比较 final pickedStartInMinutes = pickedTempTime.hour * 60 + pickedTempTime.minute; final endInMinutes = endTime.value.hour * 60 + endTime.value.minute; if (pickedStartInMinutes >= endInMinutes) { - // 在这里,我们不提示错误,而是智能地调整结束时间 + // 整结束时间 startTime.value = pickedTempTime; final newEndDateTime = tempTime.add( const Duration(minutes: 30), ); endTime.value = TimeOfDay.fromDateTime(newEndDateTime); } else { - // 合法,直接设置开始时间 + //设置开始时间 startTime.value = pickedTempTime; } } else { - // 如果当前正在设置【结束时间】,我们来和【已有的开始时间】比较 + // 如果当前正在设置结束时间,我们来和已有的开始时间比较 final pickedEndInMinutes = pickedTempTime.hour * 60 + pickedTempTime.minute; final startInMinutes = @@ -253,7 +254,6 @@ class ReservationController extends GetxController with BaseControllerMixin { showToast('结束时间必须晚于开始时间'); return; // 中断执行,不关闭弹窗 } else { - // 合法,直接设置结束时间 endTime.value = pickedTempTime; } } @@ -290,17 +290,62 @@ class ReservationController extends GetxController with BaseControllerMixin { } /// 提交预约 - void submitReservation() { - // TODO: 在这里执行提交预约的逻辑 - print('提交预约:'); - print('日期: $formattedDate'); - print('开始时间: $formattedStartTime'); - print('结束时间: $formattedEndTime'); - print('预约氢量: ${amountController.text}'); - print('车牌号: ${plateNumberController.text}'); - print('加氢站: ${selectedStation.value}'); + void submitReservation() async { + if (plateNumber.isEmpty) { + showToast("请先绑定车辆"); + return; + } + String ampuntStr = amountController.text.toString(); + if (ampuntStr.isEmpty) { + showToast("请输入需要预约的氢量"); + return; + } + if (selectedStationId.value == null || selectedStationId.value!.isEmpty) { + showToast("请先选择加氢站"); + return; + } + try { + showLoading("提交中"); + final selectedStation = stationOptions.firstWhere( + (s) => s.hydrogenId == selectedStationId.value, + ); - Get.snackbar('成功', '预约已提交!', snackPosition: SnackPosition.BOTTOM); + final dateStr = formattedDate; // "yyyy-MM-dd" + final startTimeStr = '$dateStr ${formattedStartTime}:00'; // "yyyy-MM-dd HH:mm:ss" + final endTimeStr = '$dateStr ${formattedEndTime}:00'; + + var responseData = await HttpService.to.post( + 'appointment/orderAddHyd/saveOrUpdate', + data: { + 'plateNumber': plateNumber, + 'date': dateStr, + 'startTime': startTimeStr, + 'endTime': endTimeStr, + 'stationId': selectedStationId.value, + 'stationName': selectedStation.name, + 'contacts': StorageService.to.name, + 'phone': StorageService.to.phone, + 'hydAmount': ampuntStr, + }, + ); + + if (responseData == null) { + dismissLoading(); + showToast('服务暂不可用,请稍后'); + return; + } + dismissLoading(); + + var result = BaseModel.fromJson(responseData.data); + if (result.code == 0) { + showSuccessToast("预约成功"); + } else { + showErrorToast(result.message); + } + } catch (e) { + dismissLoading(); + showToast('服务暂不可用,请稍后'); + } } /// 状态变量:是否有预约数据 @@ -319,7 +364,7 @@ class ReservationController extends GetxController with BaseControllerMixin { data: { 'phone': StorageService.to.phone, // 使用从 renderData 中获取到的 name 'pageNum': 1, - 'pageSize': 30, // 暂时不考虑分页,一次获取30条 + 'pageSize': 50, // 暂时不考虑分页,一次获取30条 }, ); @@ -618,7 +663,29 @@ class ReservationController extends GetxController with BaseControllerMixin { try { dismissLoading(); var result = BaseModel.fromJson(responseData.data); - // showToast(result.data["data"].toString()); + var stationDataList = result.data['data'] as List; + + // 使用 map 将 List 转换为 List + var stations = stationDataList + .map((item) => StationModel.fromJson(item as Map)) + .toList(); + + // 去重,确保每个 hydrogenId 唯一 + var uniqueStationsMap = {}; // 使用 Map 来去重 + for (var station in stations) { + uniqueStationsMap[station.hydrogenId] = station; // 使用 hydrogenId 作为键,确保唯一 + } + + // 获取去重后的 List + var uniqueStations = uniqueStationsMap.values.toList(); + + stationOptions.assignAll(uniqueStations); + + if (stationOptions.isEmpty) { + showToast('附近暂无可用加氢站'); + } else { + showToast('站点列表已刷新'); + } } catch (e) { showToast('数据异常'); } diff --git a/ln_jq_app/lib/pages/c_page/reservation/view.dart b/ln_jq_app/lib/pages/c_page/reservation/view.dart index 9cdfc59..240edca 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/view.dart @@ -1,34 +1,42 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; -import 'package:ln_jq_app/common/model/vehicle_info.dart'; +import 'package:ln_jq_app/common/model/station_model.dart'; import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/storage_service.dart'; import 'controller.dart'; +///加氢预约 class ReservationPage extends GetView { const ReservationPage({super.key}); @override Widget build(BuildContext context) { + Get.put(ReservationController()); return GetBuilder( - init: ReservationController(), id: 'reservation', builder: (_) { return Scaffold( backgroundColor: Colors.grey[100], - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildUserInfoCard(), - const SizedBox(height: 5), - _buildCarInfoCard(), - const SizedBox(height: 5), - _buildReservationFormCard(context), - ], + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildUserInfoCard(), + const SizedBox(height: 5), + _buildCarInfoCard(), + const SizedBox(height: 5), + _buildReservationFormCard(context), + const SizedBox(height: 5), + _buildTipsCard(), + ], + ), ), ), ); @@ -36,6 +44,38 @@ class ReservationPage extends GetView { ); } + Widget _buildTipsCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildTipItem(Icons.info_outline, '请提前30分钟到达加氢站'), + const SizedBox(height: 10), + _buildTipItem(Icons.rule, '请确保车辆证件齐全'), + const SizedBox(height: 10), + _buildTipItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-123-4567'), + ], + ), + ), + ); + } + + // 提示信息卡片中的列表项 + Widget _buildTipItem(IconData icon, String text) { + return Row( + children: [ + Icon(icon, color: Colors.blue, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)), + ), + ], + ); + } + /// 构建用户信息卡片 Widget _buildUserInfoCard() { return Card( @@ -210,7 +250,6 @@ class ReservationPage extends GetView { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16.0), - // 【修改】使用 Obx 包裹以自动响应 Rx 变量的变化 child: Obx( () => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -246,23 +285,23 @@ class ReservationPage extends GetView { enabled: false, // 设置为不可编辑 ), _buildStationSelector(), - const SizedBox(height: 24), + const SizedBox(height: 20), Row( children: [ Expanded( child: ElevatedButton( onPressed: controller.submitReservation, style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 48), + minimumSize: const Size(double.infinity, 38), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), - backgroundColor: Colors.blue, // 明确指定背景色 - foregroundColor: Colors.white, // 明确指定前景色(文字颜色) + backgroundColor: Colors.blue, + foregroundColor: Colors.white, ), child: const Text( '提交预约', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold), ), ), ), @@ -271,17 +310,17 @@ class ReservationPage extends GetView { child: OutlinedButton( onPressed: controller.getReservationList, style: OutlinedButton.styleFrom( - minimumSize: const Size(double.infinity, 48), // 高度与另一个按钮保持一致 - side: const BorderSide(color: Colors.blue), // 设置边框颜色为蓝色 + minimumSize: const Size(double.infinity, 38), // 高度与另一个按钮保持一致 + side: const BorderSide(color: Colors.blue), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), child: const Text( - '查看预约', // 假设第二个按钮是“取消” + '查看预约', style: TextStyle( - color: Colors.blue, // 设置文字颜色为蓝色 - fontSize: 16, + color: Colors.blue, + fontSize: 13, fontWeight: FontWeight.bold, ), ), @@ -308,12 +347,12 @@ class ReservationPage extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), + Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 13)), const SizedBox(height: 8), InkWell( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( border: Border.all(color: Colors.grey[400]!), borderRadius: BorderRadius.circular(8), @@ -321,7 +360,7 @@ class ReservationPage extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(value, style: const TextStyle(fontSize: 16)), + Text(value, style: const TextStyle(fontSize: 14)), Icon(icon, color: Colors.grey, size: 20), ], ), @@ -345,17 +384,19 @@ class ReservationPage extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), + Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 13)), const SizedBox(height: 8), TextFormField( controller: controller, keyboardType: keyboardType, - enabled: enabled, // 使用 enabled 参数 + enabled: enabled, + style: const TextStyle(fontSize: 14), decoration: InputDecoration( + isDense: true, hintText: hint, + hintStyle: TextStyle(fontSize: 14), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)), - contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), - // 【新增】禁用时,使用填充色 + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), filled: !enabled, fillColor: Colors.grey[100], ), @@ -366,12 +407,12 @@ class ReservationPage extends GetView { } Widget _buildStationSelector() { - return Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(0), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('加氢站', style: TextStyle(color: Colors.grey[600], fontSize: 14)), @@ -383,46 +424,109 @@ class ReservationPage extends GetView { ), ], ), - Obx( - () => DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - hint: Text( - '请选择加氢站', - style: TextStyle(fontSize: 14, color: Theme.of(Get.context!).hintColor), - ), - items: controller.stationOptions - .map( - (item) => DropdownMenuItem( - value: item, - child: _buildDropdownItem(item), // 使用自定义 Widget 构建 Item - ), - ) - .toList(), - value: controller.selectedStation.value, - onChanged: (value) { - if (value != null) { - controller.selectedStation.value = value; - } - }, - buttonStyleData: ButtonStyleData( - height: 55, // 控制按钮的高度 - padding: const EdgeInsets.symmetric(horizontal: 12.0), // 控制按钮内部的内边距 - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey[400]!), - ), - ), - iconStyleData: const IconStyleData( - icon: Icon(Icons.arrow_drop_down), - iconSize: 20, - ), - dropdownStyleData: DropdownStyleData( - maxHeight: 200, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), - ), - menuItemStyleData: const MenuItemStyleData(height: 40), + ), + Obx( + () => DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + hint: const Text( + '请选择加氢站', + style: TextStyle(fontSize: 14, color: Colors.grey), ), + // items 列表现在从 stationOptions (StationModel列表) 构建 + items: controller.stationOptions + .map( + (station) => DropdownMenuItem( + value: station.hydrogenId, // value 是站点的唯一ID + child: _buildDropdownItem(station), // child 是自定义的 Widget + ), + ) + .toList(), + value: controller.selectedStationId.value, + // 当前选中的是站点ID + onChanged: (value) { + if (value != null) { + controller.selectedStationId.value = value; + } + }, + customButton: controller.selectedStationId.value == null + ? null // 未选择时,显示默认的 hint + : _buildSelectedStationButton( + controller.stationOptions.firstWhere( + (s) => s.hydrogenId == controller.selectedStationId.value, + ), + ), + buttonStyleData: ButtonStyleData( + height: 60, // 增加高度以容纳两行文字 + padding: const EdgeInsets.symmetric(horizontal: 12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[400]!), + ), + ), + iconStyleData: const IconStyleData( + icon: Icon(Icons.arrow_drop_down, color: Colors.grey), + iconSize: 24, + ), + dropdownStyleData: DropdownStyleData( + maxHeight: 300, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + ), + menuItemStyleData: const MenuItemStyleData( + height: 60, // 增加下拉项的高度 + ), + ), + ), + ), + ], + ); + } + + /// 构建下拉菜单中的每一项 + Widget _buildDropdownItem(StationModel station) { + bool isMaintenance = station.siteStatusName == '维修中'; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Text( + '${station.name} | ¥${station.price}/kg', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + ), + if (isMaintenance) const SizedBox(width: 8), + if (isMaintenance) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.orange[100], + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '维修中', + style: TextStyle( + fontSize: 10, + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + '${station.address}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ], ), ), ], @@ -430,13 +534,21 @@ class ReservationPage extends GetView { ); } - Widget _buildDropdownItem(String item) { - return Row( - children: [ - const Icon(Icons.local_gas_station_outlined, size: 18, color: Colors.grey), - const SizedBox(width: 10), - Text(item, style: const TextStyle(fontSize: 14)), - ], + Widget _buildSelectedStationButton(StationModel station) { + return Container( + // 选中后样式 + padding: const EdgeInsets.symmetric(horizontal: 12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[400]!), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: _buildDropdownItem(station)), + const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 24), + ], + ), ); } }