import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.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'; import 'package:pull_to_refresh/pull_to_refresh.dart'; enum ReservationStatus { pending, // 待处理 ( addStatus: 0) completed, // 完成 ( addStatus: 1) rejected, // 拒绝 ( -1) unadded, // 未加 ( 2) cancel, // 取消预约 unknown, // 未知状态 } class ReservationModel { final String id; final String stationId; final String plateNumber; String amount; final String time; final String contactPerson; final String contactPhone; ReservationStatus status; // 状态是可变的 final String contacts; final String phone; final String rejectReason; final String stationName; final String startTime; final String endTime; final String date; final String hydAmount; final String state; final String stateName; final String addStatus; final String addStatusName; bool hasEdit; final String isEdit; // "1" 表示可以修改信息 // 新增附件相关字段 final int isTruckAttachment; // 1为有证件数据 0为缺少 final bool hasDrivingAttachment; // 是否有行驶证 final bool hasHydrogenationAttachment; // 是否有加氢证 ReservationModel({ required this.id, required this.stationId, required this.plateNumber, required this.amount, required this.time, required this.contactPerson, required this.contactPhone, required this.hasEdit, this.status = ReservationStatus.pending, required this.contacts, required this.phone, required this.stationName, required this.startTime, required this.endTime, required this.date, required this.hydAmount, required this.state, required this.stateName, required this.addStatus, required this.addStatusName, required this.rejectReason, required this.isTruckAttachment, required this.hasDrivingAttachment, required this.hasHydrogenationAttachment, required this.isEdit, }); /// 工厂构造函数,用于从JSON创建ReservationModel实例 factory ReservationModel.fromJson(Map json) { // 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; break; case 1: currentStatus = ReservationStatus.completed; break; case 2: currentStatus = ReservationStatus.unadded; break; case 6: currentStatus = ReservationStatus.cancel; 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) ? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm : '时间未定'; // 解析附件信息 Map attachmentVo = json['truckAttachmentVo'] ?? {}; int isTruckAttachment = attachmentVo['isTruckAttachment'] as int? ?? 0; List drivingList = attachmentVo['drivingAttachment'] ?? []; List hydrogenationList = attachmentVo['hydrogenationAttachment'] ?? []; return ReservationModel( // 原始字段,用于UI兼容 id: json['id']?.toString() ?? '', stationId: json['stationId']?.toString() ?? '', plateNumber: json['plateNumber']?.toString() ?? '---', amount: '${json['hydAmount']?.toString() ?? '0'}kg', time: timeRange, contactPerson: json['contacts']?.toString() ?? '无联系人', contactPhone: json['phone']?.toString() ?? '无联系电话', status: currentStatus, // 新增的完整字段 contacts: json['contacts']?.toString() ?? '', phone: json['phone']?.toString() ?? '', stationName: json['stationName']?.toString() ?? '', startTime: startTimeStr, endTime: endTimeStr, date: dateStr, hydAmount: json['hydAmount']?.toString() ?? '0', state: json['state']?.toString() ?? '', addStatus: statusFromServer.toString(), addStatusName: json['addStatusName']?.toString() ?? '', stateName: json['stateName']?.toString() ?? '', rejectReason: json['rejectReason']?.toString() ?? '', hasEdit: true, isTruckAttachment: isTruckAttachment, hasDrivingAttachment: drivingList.isNotEmpty, hasHydrogenationAttachment: hydrogenationList.isNotEmpty, isEdit: json['isEdit']?.toString() ?? '0', ); } } class SiteController extends GetxController with BaseControllerMixin { @override String get builderId => 'site'; SiteController(); /// 状态变量:是否有预约数据 bool hasReservationData = false; // 新增预约数据列表 List reservationList = []; Timer? _refreshTimer; final TextEditingController searchController = TextEditingController(); bool isNotice = false; final RefreshController refreshController = RefreshController(initialRefresh: false); // 加氢枪列表 final RxList gasGunList = [].obs; @override bool get listenLifecycleEvent => true; @override void onInit() { super.onInit(); renderData(); msgNotice(); startAutoRefresh(); fetchGasGunList(); } @override void onPaused() { stopAutoRefresh(); super.onPaused(); } @override void onClose() { stopAutoRefresh(); searchController.dispose(); super.onClose(); } Future msgNotice() async { final Map requestData = { 'appFlag': 1, 'isRead': 1, 'pageNum': 1, 'pageSize': 5, }; final response = await HttpService.to.get( 'appointment/unread_notice/page', params: requestData, ); if (response != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0 && result.data != null) { String total = result.data["total"].toString(); isNotice = int.parse(total) > 0; updateUi(); } } } void startAutoRefresh() { // 先停止已存在的定时器,防止重复启动 stopAutoRefresh(); // 创建一个每5分钟执行一次的周期性定时器 _refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) { renderData(); }); } void onRefresh() => renderData(isRefresh: true); ///停止定时器的方法 void stopAutoRefresh() { // 如果定时器存在并且是激活状态,就取消它 _refreshTimer?.cancel(); _refreshTimer = null; // 置为null,方便判断 print("【自动刷新】定时器已停止。"); } /// 获取加氢枪列表 Future fetchGasGunList() async { try { var response = await HttpService.to.get( 'appointment/station/getGasGunList?hydrogenId=${StorageService.to.userId}', ); if (response != null && response.data != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0 && result.data != null) { List dataList = result.data as List; gasGunList.assignAll(dataList.map((e) => e['deviceName'].toString()).toList()); } } } catch (e) { Logger.d("获取加氢枪列表失败: $e"); } } /// 获取预约数据的方法 Future fetchReservationData() async { showLoading("加载中"); final String searchText = searchController.text.trim(); try { var response = await HttpService.to.post( "appointment/orderAddHyd/sitOrderPage", data: { 'stationName': name, // 使用从 renderData 中获取到的 name 'pageNum': 1, 'pageSize': 50, // 暂时不考虑分页,一次获取30条 'keyword': searchText, // 加氢站名称、手机号 'stationId': StorageService.to.userId, }, ); // 安全校验 if (response == null || response.data == null) { showToast('暂时无法获取预约数据'); hasReservationData = false; reservationList = []; dismissLoading(); return; } final baseModel = BaseModel.fromJson(response.data); if (baseModel.code == 0 && baseModel.data != null) { // 【核心修改】处理接口返回的列表数据 final dataMap = baseModel.data as Map; final List listFromServer = dataMap['records'] ?? []; // 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象 reservationList = listFromServer.map((item) { return ReservationModel.fromJson(item as Map); }).toList(); // 根据列表是否为空来更新 hasReservationData 状态 hasReservationData = reservationList.isNotEmpty; } else { // 接口返回业务错误 showToast(baseModel.message); hasReservationData = false; reservationList = []; // 清空列表 } } catch (e) { // 捕获网络或解析异常 showToast('获取预约数据失败'); hasReservationData = false; reservationList = []; // 清空列表 } finally { // 无论成功失败,最后都要关闭加载动画并更新UI dismissLoading(); updateUi(); } } /// 确认预约弹窗重构 Future confirmReservation(String id, {bool isEdit = false}) async { final item = reservationList.firstWhere( (item) => item.id == id, orElse: () => throw Exception('Reservation not found'), ); // 加氢量保留3位小数 double initialAmount = double.tryParse(item.hydAmount) ?? 0.0; final TextEditingController amountController = TextEditingController( text: initialAmount.toStringAsFixed(3), ); final RxString selectedGun = (gasGunList.isNotEmpty ? gasGunList.first : '').obs; final RxBool isOfflineChecked = false.obs; Get.dialog( Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isEdit ? '修改加氢量' : '确认加氢状态', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), // 车牌号及标签 Row( children: [ Text( item.plateNumber == "---" ? '-------' : item.plateNumber, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, color: item.plateNumber == "---" ? Colors.grey : Colors.black, letterSpacing: item.plateNumber == "---" ? 2 : 0, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Color.fromRGBO(232, 243, 255, 1), borderRadius: BorderRadius.circular(4), ), child: Text( item.plateNumber == "---" ? '车牌号识别' : '重新识别', style: TextStyle( color: Color.fromRGBO(22, 93, 255, 1), fontSize: 13.sp, fontWeight: FontWeight.bold, ), ), ), SizedBox(width: 16.w), if (item.plateNumber != "---" && item.hasDrivingAttachment) _buildInfoTag('行驶证'), if (item.plateNumber != "---" && item.hasHydrogenationAttachment) _buildInfoTag('加氢证'), ], ), const SizedBox(height: 12), // 提示逻辑 if (isEdit) const Text( '每个订单只能修改一次,请确认加氢量准确无误', style: TextStyle(color: Colors.red, fontSize: 12), ) else if (item.plateNumber == "---" || item.isTruckAttachment == 0) Row( children: [ const Expanded( child: Text( '车辆未上传加氢证,请完成线下登记', style: TextStyle(color: Colors.red, fontSize: 12), ), ), Obx( () => Checkbox( value: isOfflineChecked.value, onChanged: (v) => isOfflineChecked.value = v ?? false, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, activeColor: AppTheme.themeColor, ), ), ], ), const SizedBox(height: 16), // 预定加氢量输入区 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF7F8FA), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '预定加氢量', style: TextStyle( color: Color.fromRGBO(51, 51, 51, 1), fontSize: 14.sp, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ IntrinsicWidth( child: TextField( controller: amountController, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), inputFormatters: [ // 限制最多输入3位小数 FilteringTextInputFormatter.allow( RegExp(r'^\d+\.?\d{0,3}'), ), ], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFF017143), ), decoration: const InputDecoration( enabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: Color(0xFF017143), ), ), focusedBorder: const UnderlineInputBorder( borderSide: BorderSide( color: Color(0xFF017143), ), ), isDense: true, contentPadding: EdgeInsets.zero, ), ), ), const Text( ' KG', style: TextStyle(color: Colors.grey, fontSize: 14), ), ], ), ], ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: const Color(0xFF017143), borderRadius: BorderRadius.circular(8), ), child: const Row( children: [ Icon(Icons.qr_code_scanner, color: Colors.white, size: 18), SizedBox(width: 4), Text( '识别', style: TextStyle(color: Colors.white, fontSize: 14), ), ], ), ), ], ), ), const SizedBox(height: 16), // 加氢枪号选择 const Text( '请选择加氢枪号', style: TextStyle(color: Colors.grey, fontSize: 12), ), const SizedBox(height: 8), Obx( () => Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: selectedGun.value.isEmpty ? null : selectedGun.value, isExpanded: true, hint: const Text('请选择加氢枪号'), items: gasGunList.map((String gun) { return DropdownMenuItem( value: gun, child: Text(gun)); }).toList(), onChanged: (v) => selectedGun.value = v ?? '', ), ), ), ), const SizedBox(height: 24), // 按钮 Row( children: [ Expanded( flex: 2, child: ElevatedButton( onPressed: () { //加氢后 订单编辑 if (isEdit) { final num addHydAmount = num.tryParse(amountController.text) ?? 0; upDataService( id, 0, 1, addHydAmount, "", item, gunNumber: selectedGun.value, plateNumber: item.plateNumber, isEdit: true ); Get.back(); return; } //订单确认 if (!isEdit && (item.plateNumber == "---" || item.isTruckAttachment == 0) && !isOfflineChecked.value) { showToast("车辆未上传加氢证 , 请确保线下登记后点击确认"); return; } if (selectedGun.value.isEmpty) { showToast("请选择加氢枪号"); return; } Get.back(); final num addHydAmount = num.tryParse(amountController.text) ?? 0; upDataService( id, 0, 1, addHydAmount, "", item, gunNumber: selectedGun.value, plateNumber: item.plateNumber, ); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF017143), minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 0, ), child: Text( isEdit ? '确认修改' : '确认加氢', style: const TextStyle(color: Colors.white, fontSize: 16), ), ), ), const SizedBox(width: 12), Expanded( flex: 1, child: OutlinedButton( onPressed: () { Get.back(); if (!isEdit) { upDataService( id, 0, 2, 0, "", item, gunNumber: selectedGun.value, plateNumber: item.plateNumber, ); } }, style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), side: BorderSide(color: Colors.grey.shade300), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( isEdit ? '取消' : '未加氢', style: const TextStyle(color: Colors.grey, fontSize: 16), ), ), ), ], ), const SizedBox(height: 12), Row( children: [ const Expanded( child: Divider(color: Color(0xFFEEEEEE), thickness: 1), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: GestureDetector( onTap: () => Get.back(), child: Text( '暂不处理', style: TextStyle( color: Color.fromRGBO(16, 185, 129, 1), fontSize: 14.sp, ), ), ), ), const Expanded( child: Divider(color: Color(0xFFEEEEEE), thickness: 1), ), ], ), ], ), ), ), ), barrierDismissible: true, ); } Widget _buildInfoTag(String label) { return Container( margin: const EdgeInsets.only(left: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFF2F3F5), borderRadius: BorderRadius.circular(4), ), child: Text( label, style: TextStyle(color: Color(0xFF999999), fontSize: 11.sp), ), ); } Future rejectReservation(String id) async { final item = reservationList.firstWhere((item) => item.id == id); final TextEditingController reasonController = TextEditingController(); final RxString selectedReason = ''.obs; // 预设的拒绝原因列表 final List presetReasons = ['车辆未到场', '司机联系不上', '设备故障无法加氢']; Get.dialog( Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( '拒绝预约', textAlign: TextAlign.center, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Text( '正在处理车牌号 ${item.plateNumber} 的预约', textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, color: AppTheme.themeColor), ), const SizedBox(height: 24), // 4. 预设原因选择区域 const Text('选择或填写拒绝原因:', style: TextStyle(color: Colors.grey)), const SizedBox(height: 8), Obx( () => Wrap( // 使用 Wrap 自动换行 spacing: 8.0, // 水平间距 children: presetReasons.map((reason) { final isSelected = selectedReason.value == reason; return ChoiceChip( label: Text(reason), selected: isSelected, onSelected: (selected) { if (selected) { selectedReason.value = reason; reasonController.clear(); // 选择预设原因时,清空自定义输入 } }, selectedColor: Get.theme.primaryColor.withOpacity(0.2), labelStyle: TextStyle( color: isSelected ? Get.theme.primaryColor : Colors.black, ), ); }).toList(), ), ), const SizedBox(height: 16), // 5. 自定义原因输入框 TextField( controller: reasonController, maxLines: 2, decoration: InputDecoration( hintText: '输入其它原因', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300), ), ), onChanged: (text) { if (text.isNotEmpty) { selectedReason.value = ''; // 输入自定义原因时,取消预设选择 } }, ), const SizedBox(height: 24), // 6. 按钮区域 Row( children: [ Expanded( child: TextButton( onPressed: () { // 获取最终的拒绝原因 String finalReason = reasonController.text.trim(); if (finalReason.isEmpty) { finalReason = selectedReason.value; } if (finalReason.isEmpty) { showToast('请选择或填写一个拒绝原因'); return; } Get.back(); // 关闭弹窗 upDataService( id, 1, -1, 0, finalReason, item, plateNumber: item.plateNumber, ); }, 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, ); } //addStatus 1完成 2未加 -1拒绝 void upDataService(String id, int status, int addStatus, num addHydAmount, String rejectReason, ReservationModel item, { String? gunNumber, String? plateNumber, bool isEdit = false }) async { showLoading("确认中"); try { var responseData; if (isEdit) { responseData = await HttpService.to.post( 'appointment/orderAddHyd/modifyOrder', data: { 'id': id, "addHydAmount": addHydAmount, "plateNumber": plateNumber, if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber, }, ); }else if (addStatus == -1) { responseData = await HttpService.to.post( 'appointment/orderAddHyd/rejectOrder', data: { 'id': id, 'state': -1, //拒绝使用 "rejectReason": rejectReason, "plateNumber": plateNumber, if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber, }, ); } else { responseData = await HttpService.to.post( 'appointment/orderAddHyd/completeOrder', data: { 'id': id, 'addStatus': addStatus, //完成使用 完成1,未加2 "addHydAmount": addHydAmount, "plateNumber": plateNumber, if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber, }, ); } if (responseData == null || responseData.data == null) { dismissLoading(); showToast('服务暂不可用,请稍后'); return; } var result = BaseModel.fromJson(responseData.data); if (result.code == 0) { showSuccessToast("操作成功"); } else { showToast(result.message); } dismissLoading(); //1完成 2未加 -1拒绝 if (addStatus == 1) { item.status = ReservationStatus.completed; item.amount = "${addHydAmount}kg"; } else if (addStatus == -1) { item.status = ReservationStatus.rejected; } else if (addStatus == 2) { item.status = ReservationStatus.unadded; } renderData(); } catch (e) { dismissLoading(); } } String leftHydrogen = ""; String orderAmount = ""; String completedAmount = ""; String name = ""; String orderTotalAmount = ""; String orderUnfinishedAmount = ""; Future renderData({bool isRefresh = false}) async { try { var responseData = await HttpService.to.get( 'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}', ); if (responseData == null || responseData.data == null) { showToast('暂时无法获取站点信息'); return; } try { var result = BaseModel.fromJson(responseData.data); leftHydrogen = result.data["leftHydrogen"] ?? ""; 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(); orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString(); orderUnfinishedAmount = orderUnfinishedAmount.isEmpty ? "统计中" : orderUnfinishedAmount.toString(); } catch (e) { showToast('数据异常'); } } catch (e) {} finally { //加载列表数据 fetchReservationData(); if (isRefresh) { refreshController.refreshCompleted(); } } } }