ocr识别,历史新增类型

This commit is contained in:
2026-02-28 17:15:42 +08:00
parent b7caf58adf
commit 0dded3b928
2 changed files with 219 additions and 62 deletions

View File

@@ -1,7 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.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/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel 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; final RxBool hasData = false.obs;
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value); String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value); String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
String stationName = ""; String stationName = "";
final Map<String, String> statusOptions = { final Map<String, String> statusOptions = {
'': '全部', '': '全部',
'100': '未预约加氢',
'0': '待加氢', '0': '待加氢',
'1': '已加氢', '1': '已加氢',
'2': '未加氢', '2': '未加氢',

View File

@@ -1,16 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.dart' as dio;
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:image_gallery_saver/image_gallery_saver.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/model/base_model.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/storage_service.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.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
@@ -182,6 +181,8 @@ class SiteController extends GetxController with BaseControllerMixin {
// 加氢枪列表 // 加氢枪列表
final RxList<String> gasGunList = <String>[].obs; final RxList<String> gasGunList = <String>[].obs;
final RxMap<String, Map<String, dynamic>> gasGunMap =
<String, Map<String, dynamic>>{}.obs;
@override @override
bool get listenLifecycleEvent => true; bool get listenLifecycleEvent => true;
@@ -229,11 +230,9 @@ class SiteController extends GetxController with BaseControllerMixin {
} }
} }
/// 创建一个每5分钟执行一次的周期性定时器
void startAutoRefresh() { void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh(); stopAutoRefresh();
// 创建一个每5分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) { _refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
renderData(); renderData();
}); });
@@ -241,12 +240,10 @@ class SiteController extends GetxController with BaseControllerMixin {
void onRefresh() => renderData(isRefresh: true); void onRefresh() => renderData(isRefresh: true);
///停止定时器的方法 ///停止定时器
void stopAutoRefresh() { void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel(); _refreshTimer?.cancel();
_refreshTimer = null; // 置为null方便判断 _refreshTimer = null;
print("【自动刷新】定时器已停止。");
} }
/// 获取加氢枪列表 /// 获取加氢枪列表
@@ -259,7 +256,17 @@ class SiteController extends GetxController with BaseControllerMixin {
final result = BaseModel<dynamic>.fromJson(response.data); final result = BaseModel<dynamic>.fromJson(response.data);
if (result.code == 0 && result.data != null) { if (result.code == 0 && result.data != null) {
List dataList = result.data as List; 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<String, dynamic>.from(item);
}
} }
} }
} catch (e) { } catch (e) {
@@ -327,14 +334,20 @@ class SiteController extends GetxController with BaseControllerMixin {
} }
} }
/// 确认预约弹窗重构 /// 确认预约弹窗
Future<void> confirmReservation( Future<void> confirmReservation(
String id, { String id, {
bool isEdit = false, bool isEdit = false,
bool isAdd = false, bool isAdd = false,
}) async { }) async {
ReservationModel item; ReservationModel item;
if (isAdd) { //处理是否是无预约车辆加氢数据
if (!isAdd) {
item = reservationList.firstWhere(
(item) => item.id == id,
orElse: () => throw Exception('Reservation not found'),
);
} else {
// 如果是无预约车辆加氢,创建一个临时 model // 如果是无预约车辆加氢,创建一个临时 model
item = ReservationModel( item = ReservationModel(
id: "", id: "",
@@ -364,13 +377,13 @@ class SiteController extends GetxController with BaseControllerMixin {
drivingAttachments: [], drivingAttachments: [],
hydrogenationAttachments: [], 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位小数 // 加氢量保留3位小数
double initialAmount = double.tryParse(item.hydAmount) ?? 0.0; double initialAmount = double.tryParse(item.hydAmount) ?? 0.0;
final TextEditingController amountController = TextEditingController( final TextEditingController amountController = TextEditingController(
@@ -382,6 +395,7 @@ class SiteController extends GetxController with BaseControllerMixin {
Get.dialog( Get.dialog(
Dialog( Dialog(
insetPadding: EdgeInsets.only(left: 20.w, right: 20.w),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
@@ -399,33 +413,53 @@ class SiteController extends GetxController with BaseControllerMixin {
// 车牌号及标签 // 车牌号及标签
Row( Row(
children: [ children: [
Text( Container(
item.plateNumber == "---" ? '-------' : item.plateNumber, width: 80.w,
style: TextStyle( child: TextField(
fontSize: 16.sp, controller: plateController,
fontWeight: FontWeight.w500, style: TextStyle(
color: item.plateNumber == "---" ? Colors.grey : Colors.black, color: const Color.fromRGBO(51, 51, 51, 1),
letterSpacing: item.plateNumber == "---" ? 2 : 0, 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), const SizedBox(width: 8),
isEdit isEdit
? SizedBox() ? SizedBox()
: Container( : GestureDetector(
padding: const EdgeInsets.symmetric( onTap: () async {
horizontal: 8, String? temp = await takePhotoAndRecognize(true);
vertical: 2, if (temp != null && temp.isNotEmpty) {
), plateController.text = temp;
decoration: BoxDecoration( }
color: Color.fromRGBO(232, 243, 255, 1), },
borderRadius: BorderRadius.circular(4), child: Container(
), padding: const EdgeInsets.symmetric(
child: Text( horizontal: 8,
item.plateNumber == "---" ? '车牌号识别' : '重新识别', vertical: 2,
style: TextStyle( ),
color: Color.fromRGBO(22, 93, 255, 1), decoration: BoxDecoration(
fontSize: 13.sp, color: Color.fromRGBO(232, 243, 255, 1),
fontWeight: FontWeight.bold, 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), GestureDetector(
decoration: BoxDecoration( onTap: () async {
color: const Color(0xFF017143), String? temp = await takePhotoAndRecognize(
borderRadius: BorderRadius.circular(8), false,
), deviceName: selectedGun.value,
child: const Row( sign: getSignByDeviceName(selectedGun.value),
children: [ );
Icon(Icons.qr_code_scanner, color: Colors.white, size: 18), if (temp != null && temp.isNotEmpty) {
SizedBox(width: 4), amountController.text = temp;
Text( }
'识别', },
style: TextStyle(color: Colors.white, fontSize: 14), 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, item,
gunNumber: selectedGun.value, gunNumber: selectedGun.value,
plateNumber: item.plateNumber, plateNumber: plateController.text,
isAdd: true, isAdd: true,
); );
Get.back(); Get.back();
@@ -655,7 +709,7 @@ class SiteController extends GetxController with BaseControllerMixin {
"", "",
item, item,
gunNumber: selectedGun.value, gunNumber: selectedGun.value,
plateNumber: item.plateNumber, plateNumber: plateController.text,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -687,7 +741,7 @@ class SiteController extends GetxController with BaseControllerMixin {
"", "",
item!, item!,
gunNumber: selectedGun.value, 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<String?> 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<String?> 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<String?> 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<String?> 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 leftHydrogen = "";
String orderAmount = ""; String orderAmount = "";
String completedAmount = ""; String completedAmount = "";