import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:getx_scaffold/getx_scaffold.dart' as dio; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_picker/image_picker.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:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.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; // 是否有加氢证 final List drivingAttachments; // 行驶证图片列表 final List hydrogenationAttachments; // 加氢证图片列表 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, required this.drivingAttachments, required this.hydrogenationAttachments, }); /// 工厂构造函数,用于从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', drivingAttachments: drivingList.map((e) => e.toString()).toList(), hydrogenationAttachments: hydrogenationList.map((e) => e.toString()).toList(), ); } } 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; final RxMap> gasGunMap = >{}.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(); } } } /// 创建一个每5分钟执行一次的周期性定时器 void startAutoRefresh() { stopAutoRefresh(); _refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) { renderData(); }); } void onRefresh() => renderData(isRefresh: true); ///停止定时器 void stopAutoRefresh() { _refreshTimer?.cancel(); _refreshTimer = null; } /// 获取加氢枪列表 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.clear(); gasGunMap.clear(); for (var item in dataList) { String name = item['deviceName'].toString(); // 将名称加入列表供 Dropdown/Picker 使用 gasGunList.add(name); // 将完整对象存入 Map,方便后续通过 name 获取 sign gasGunMap[name] = Map.from(item); } } } } 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, bool isAdd = false, }) async { ReservationModel item; //处理是否是无预约车辆加氢数据 if (!isAdd) { item = reservationList.firstWhere( (item) => item.id == id, orElse: () => throw Exception('Reservation not found'), ); } else { // 如果是无预约车辆加氢,创建一个临时 model item = ReservationModel( id: "", stationId: StorageService.to.userId ?? "", plateNumber: "---", amount: "0.000", time: "", contactPerson: "", contactPhone: "", hasEdit: true, contacts: "", phone: "", stationName: name, startTime: "", endTime: "", date: "", hydAmount: "0.000", state: "", stateName: "", addStatus: "", addStatusName: "", rejectReason: "", isTruckAttachment: 0, hasDrivingAttachment: false, hasHydrogenationAttachment: false, isEdit: "0", drivingAttachments: [], hydrogenationAttachments: [], ); } //车牌输入 final TextEditingController plateController = TextEditingController( text: item.plateNumber == "---" ? "" : item.plateNumber, ); // 加氢量保留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( insetPadding: EdgeInsets.only(left: 20.w, right: 20.w), 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( isAdd ? "无预约车辆加氢" : (isEdit ? '修改加氢量' : '确认加氢状态'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), // 车牌号及标签 Row( children: [ Container( width: 80.w, child: TextField( controller: plateController, style: TextStyle( color: const Color.fromRGBO(51, 51, 51, 1), fontSize: 14.sp, fontWeight: FontWeight.bold, ), decoration: InputDecoration( hintText: item.plateNumber == "---" ? '_ _ _ _ _ _' : '修正车牌', hintStyle: TextStyle( color: const Color.fromRGBO(51, 51, 51, 1), fontSize: 13.sp, ), border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, ), ), ), const SizedBox(width: 8), isEdit ? SizedBox() : GestureDetector( onTap: () async { String? temp = await takePhotoAndRecognize(true); if (temp != null && temp.isNotEmpty) { plateController.text = temp; } }, child: 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('行驶证', item.drivingAttachments), if (item.plateNumber != "---" && item.hasHydrogenationAttachment) buildInfoTag('加氢证', item.hydrogenationAttachments), ], ), SizedBox(height: 6.h), // 提示逻辑 if (isEdit) Text( '每个订单只能修改一次,请确认加氢量准确无误', style: TextStyle( color: Colors.red, fontSize: 12.sp, fontWeight: FontWeight.w400, ), ) else if (item.plateNumber == "---" || item.isTruckAttachment == 0) Row( children: [ Expanded( child: Text( '车辆未上传加氢证,请完成线下登记', style: TextStyle( color: Colors.red, fontSize: 12.sp, fontWeight: FontWeight.w400, ), ), ), Obx( () => Checkbox( value: isOfflineChecked.value, onChanged: (v) => isOfflineChecked.value = v ?? false, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, activeColor: AppTheme.themeColor, ), ), ], ), SizedBox(height: 6.h), // 预定加氢量输入区 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), ), ], ), ], ), ), GestureDetector( onTap: () async { String? temp = await takePhotoAndRecognize( false, deviceName: selectedGun.value, sign: getSignByDeviceName(selectedGun.value), ); if (temp != null && temp.isNotEmpty) { amountController.text = temp; } }, child: 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.camera_alt_outlined, 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; } //无预约订单 if (isAdd) { final num addHydAmount = num.tryParse(amountController.text) ?? 0; upDataService( id, 0, 1, addHydAmount, "", item, gunNumber: selectedGun.value, plateNumber: plateController.text, isAdd: true, ); Get.back(); return; } //有预约订单确认 Get.back(); final num addHydAmount = num.tryParse(amountController.text) ?? 0; upDataService( id, 0, 1, addHydAmount, "", item, gunNumber: selectedGun.value, plateNumber: plateController.text, ); }, 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 && !isAdd) { upDataService( id, 0, 2, 0, "", item!, gunNumber: selectedGun.value, plateNumber: plateController.text, ); } }, style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), side: BorderSide(color: Colors.grey.shade300), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( isEdit || isAdd ? '取消' : '未加氢', 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: false, ); } /// 保存图片到相册 Future saveImageToLocal(String url) async { try { // 1. 权限请求 if (Platform.isAndroid) { var status = await Permission.storage.request(); if (!status.isGranted) { showErrorToast("请在系统设置中开启存储权限"); return; } } else { var status = await Permission.photos.request(); if (!status.isGranted) { showErrorToast("请在系统设置中开启相册权限"); return; } } showLoading("正在保存..."); // 2. 下载图片 var response = await Dio().get( url, options: Options(responseType: ResponseType.bytes), ); // 3. 保存到相册 final result = await ImageGallerySaver.saveImage( Uint8List.fromList(response.data), quality: 100, name: "certificate_${DateTime.now().millisecondsSinceEpoch}", ); dismissLoading(); if (result != null && result['isSuccess'] == true) { showSuccessToast("图片已保存至相册"); } else { showErrorToast("保存失败"); } } catch (e) { dismissLoading(); showErrorToast("保存异常"); } } Widget buildInfoTag(String label, List images) { return GestureDetector( onTap: () { showImagePreview(images); }, child: 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), ), ), ); } /// 显示图片预览弹窗 void showImagePreview(List images) { if (images.isEmpty) return; final RxInt currentIndex = 0.obs; final PageController pageController = PageController(); Get.dialog( GestureDetector( onTap: () => Get.back(), child: Stack( alignment: Alignment.center, children: [ Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ // 图片翻页 SizedBox( height: Get.height * 0.5, child: PhotoViewGallery.builder( scrollPhysics: const BouncingScrollPhysics(), builder: (BuildContext context, int index) { return PhotoViewGalleryPageOptions( imageProvider: NetworkImage(images[index]), initialScale: PhotoViewComputedScale.contained, heroAttributes: PhotoViewHeroAttributes(tag: images[index]), onTapDown: (context, details, controllerValue) { _showSaveImageMenu(images[index]); }, ); }, itemCount: images.length, loadingBuilder: (context, event) => const Center( child: CircularProgressIndicator(color: Colors.white), ), backgroundDecoration: const BoxDecoration( color: Colors.transparent, ), pageController: pageController, onPageChanged: (index) => currentIndex.value = index, ), ), SizedBox(height: 10.h), // 页码指示器 Center( child: Text( "${currentIndex.value + 1} / ${images.length}", style: const TextStyle( color: Colors.white, fontSize: 16, decoration: TextDecoration.none, ), ), ), ], ), ), // 关闭按钮 Positioned( top: 150.h, right: 20, child: IconButton( icon: const Icon(Icons.close, color: Colors.white, size: 30), onPressed: () => Get.back(), ), ), ], ), ), useSafeArea: false, ); } void _showSaveImageMenu(String url) { Get.bottomSheet( Container( color: Colors.white, child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.download), title: const Text('保存图片到相册'), onTap: () { Get.back(); saveImageToLocal(url); }, ), const Divider(height: 1), ListTile( title: const Text('取消', textAlign: TextAlign.center), onTap: () => Get.back(), ), ], ), ), ), ); } 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, bool isAdd = false, }) async { showLoading("确认中"); try { var responseData; if (isAdd) { responseData = await HttpService.to.post( 'appointment/orderAddHyd/addOfflineOrder', data: { "addHydAmount": addHydAmount, "plateNumber": plateNumber, if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber, }, ); } else 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(); 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(); } } //车牌&加氢量 识别 Future takePhotoAndRecognize( bool isPlate, { String deviceName = "", String sign = "", }) async { var status = await Permission.camera.request(); if (!status.isGranted) { if (status.isPermanentlyDenied) openAppSettings(); showErrorToast("需要相机权限才能拍照识别"); return ""; } final XFile? photo = await ImagePicker().pickImage( source: ImageSource.camera, imageQuality: 80, // 压缩图片质量以加快上传 ); if (photo == null) { return ""; } //上传文件 String? imageUrl = await uploadFile(photo.path); String? ocrStr = ""; if (imageUrl != null) { // 获取车牌号 if (isPlate) { ocrStr = await getPlateNumber(imageUrl); } else { ocrStr = await getHyd(imageUrl, deviceName, sign); } return ocrStr; } return ""; } String getSignByDeviceName(String deviceName) { return gasGunMap[deviceName]?['sign']?.toString() ?? ''; } /// 上传图片 Future uploadFile(String filePath) async { showLoading("正在上传图片..."); try { dio.FormData formData = dio.FormData.fromMap({ 'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_plate.jpg'), }); var response = await HttpService.to.post("appointment/ocr/upload", data: formData); if (response != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0) return result.data.toString(); showErrorToast(result.error); } } catch (e) { showErrorToast("图片上传失败"); } finally { dismissLoading(); } return null; } /// OCR 识别 Future getPlateNumber(String imageUrl) async { showLoading("正在识别车牌..."); try { var response = await HttpService.to.get( "appointment/ocr/getPlateNumber", params: {'imageUrl': imageUrl}, ); if (response != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0) return result.data.toString(); showErrorToast(result.error); } } catch (e) { showErrorToast("车牌识别失败"); } finally { dismissLoading(); } return null; } //加氢量识别 (加油枪列表接口返回的deviceName) (加油枪列表接口返回的sign) Future getHyd(String imageUrl, String deviceName, String sign) async { showLoading("正在识别加氢量..."); try { var response = await HttpService.to.post( "appointment/hyd-ocr/get-info", data: {"url": imageUrl, "deviceName": deviceName, "sign": sign}, ); if (response != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0) return result.data["mass"].toString(); showErrorToast(result.error); } } catch (e) { showErrorToast("车牌识别失败"); } finally { dismissLoading(); } return null; } 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(); } } } }