diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart index 45d6f58..ef4c9dc 100644 --- a/ln_jq_app/lib/pages/b_page/history/controller.dart +++ b/ln_jq_app/lib/pages/b_page/history/controller.dart @@ -1,7 +1,4 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.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'; // Reuse ReservationModel @@ -25,12 +22,14 @@ class HistoryController extends GetxController with BaseControllerMixin { final RxBool hasData = false.obs; String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value); + String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value); String stationName = ""; final Map statusOptions = { '': '全部', + '100': '未预约加氢', '0': '待加氢', '1': '已加氢', '2': '未加氢', diff --git a/ln_jq_app/lib/pages/b_page/site/controller.dart b/ln_jq_app/lib/pages/b_page/site/controller.dart index 46941ab..e87f903 100644 --- a/ln_jq_app/lib/pages/b_page/site/controller.dart +++ b/ln_jq_app/lib/pages/b_page/site/controller.dart @@ -1,16 +1,15 @@ import 'dart:async'; import 'dart:io'; -import 'package:dio/dio.dart'; 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:ln_jq_app/common/login_util.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:permission_handler/permission_handler.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'; @@ -182,6 +181,8 @@ class SiteController extends GetxController with BaseControllerMixin { // 加氢枪列表 final RxList gasGunList = [].obs; + final RxMap> gasGunMap = + >{}.obs; @override bool get listenLifecycleEvent => true; @@ -229,11 +230,9 @@ class SiteController extends GetxController with BaseControllerMixin { } } + /// 创建一个每5分钟执行一次的周期性定时器 void startAutoRefresh() { - // 先停止已存在的定时器,防止重复启动 stopAutoRefresh(); - - // 创建一个每5分钟执行一次的周期性定时器 _refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) { renderData(); }); @@ -241,12 +240,10 @@ class SiteController extends GetxController with BaseControllerMixin { void onRefresh() => renderData(isRefresh: true); - ///停止定时器的方法 + ///停止定时器 void stopAutoRefresh() { - // 如果定时器存在并且是激活状态,就取消它 _refreshTimer?.cancel(); - _refreshTimer = null; // 置为null,方便判断 - print("【自动刷新】定时器已停止。"); + _refreshTimer = null; } /// 获取加氢枪列表 @@ -259,7 +256,17 @@ class SiteController extends GetxController with BaseControllerMixin { 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()); + + 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) { @@ -327,14 +334,20 @@ class SiteController extends GetxController with BaseControllerMixin { } } - /// 确认预约弹窗重构 + /// 确认预约弹窗 Future confirmReservation( String id, { bool isEdit = false, bool isAdd = false, }) async { ReservationModel item; - if (isAdd) { + //处理是否是无预约车辆加氢数据 + if (!isAdd) { + item = reservationList.firstWhere( + (item) => item.id == id, + orElse: () => throw Exception('Reservation not found'), + ); + } else { // 如果是无预约车辆加氢,创建一个临时 model item = ReservationModel( id: "", @@ -364,13 +377,13 @@ class SiteController extends GetxController with BaseControllerMixin { drivingAttachments: [], hydrogenationAttachments: [], ); - } else { - item = reservationList.firstWhere( - (item) => item.id == id, - orElse: () => throw Exception('Reservation not found'), - ); } + //车牌输入 + final TextEditingController plateController = TextEditingController( + text: item.plateNumber == "---" ? "" : item.plateNumber, + ); + // 加氢量保留3位小数 double initialAmount = double.tryParse(item.hydAmount) ?? 0.0; final TextEditingController amountController = TextEditingController( @@ -382,6 +395,7 @@ class SiteController extends GetxController with BaseControllerMixin { Get.dialog( Dialog( + insetPadding: EdgeInsets.only(left: 20.w, right: 20.w), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: SingleChildScrollView( child: Padding( @@ -399,33 +413,53 @@ class SiteController extends GetxController with BaseControllerMixin { // 车牌号及标签 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, + 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() - : 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, + : 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, + ), ), ), ), @@ -539,21 +573,41 @@ class SiteController extends GetxController with BaseControllerMixin { ], ), ), - 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), - ), - ], + + 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), + ), + ], + ), ), ), ], @@ -637,7 +691,7 @@ class SiteController extends GetxController with BaseControllerMixin { "", item, gunNumber: selectedGun.value, - plateNumber: item.plateNumber, + plateNumber: plateController.text, isAdd: true, ); Get.back(); @@ -655,7 +709,7 @@ class SiteController extends GetxController with BaseControllerMixin { "", item, gunNumber: selectedGun.value, - plateNumber: item.plateNumber, + plateNumber: plateController.text, ); }, style: ElevatedButton.styleFrom( @@ -687,7 +741,7 @@ class SiteController extends GetxController with BaseControllerMixin { "", item!, gunNumber: selectedGun.value, - plateNumber: item.plateNumber, + plateNumber: plateController.text, ); } }, @@ -736,7 +790,7 @@ class SiteController extends GetxController with BaseControllerMixin { ), ), ), - barrierDismissible: true, + barrierDismissible: false, ); } @@ -1122,6 +1176,110 @@ class SiteController extends GetxController with BaseControllerMixin { } } + //车牌&加氢量 识别 + 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 = "";