ocr识别,历史新增类型
This commit is contained in:
@@ -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<String, String> statusOptions = {
|
||||
'': '全部',
|
||||
'100': '未预约加氢',
|
||||
'0': '待加氢',
|
||||
'1': '已加氢',
|
||||
'2': '未加氢',
|
||||
|
||||
@@ -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<String> gasGunList = <String>[].obs;
|
||||
final RxMap<String, Map<String, dynamic>> gasGunMap =
|
||||
<String, Map<String, dynamic>>{}.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<dynamic>.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<String, dynamic>.from(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -327,14 +334,20 @@ class SiteController extends GetxController with BaseControllerMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// 确认预约弹窗重构
|
||||
/// 确认预约弹窗
|
||||
Future<void> 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<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 orderAmount = "";
|
||||
String completedAmount = "";
|
||||
|
||||
Reference in New Issue
Block a user