v1.2.1 待发布

This commit is contained in:
2025-12-22 16:15:32 +08:00
parent 21a528d6d1
commit 9ba152b3c3
5 changed files with 312 additions and 272 deletions

View File

@@ -1,17 +1,13 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:zxing_lib/common.dart';
import 'package:zxing_lib/qrcode.dart';
import 'package:zxing_lib/zxing.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:permission_handler/permission_handler.dart';
class QrCodeController extends GetxController
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
@@ -22,11 +18,11 @@ class QrCodeController extends GetxController
late final AnimationController animationController;
late final Animation<double> scanAnimation;
// --- QR Scanning ---
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? qrViewController;
final Rx<Barcode?> result = Rx<Barcode?>(null);
// --- 使用 MobileScanner 的控制器 ---
final MobileScannerController scannerController = MobileScannerController();
final RxBool isFlashOn = false.obs;
final RxBool isProcessingResult = false.obs;
@override
void onInit() {
@@ -37,140 +33,114 @@ class QrCodeController extends GetxController
duration: const Duration(milliseconds: 2500),
vsync: this,
);
scanAnimation = Tween<double>(begin: 0, end: 1).animate(animationController);
scanAnimation =
Tween<double>(begin: 0, end: 1).animate(animationController);
animationController.repeat(reverse: false);
}
/// 当 QRView 创建时调用
void onQRViewCreated(QRViewController controller) {
this.qrViewController = controller;
// 监听扫描到的数据
controller.scannedDataStream.listen((scanData) {
if (scanData.code != null && result.value?.code != scanData.code) {
result.value = scanData;
qrViewController?.pauseCamera();
/// MobileScanner 的 onDetect 回调方法
void onDetect(BarcodeCapture capture) {
if (isProcessingResult.value) return;
animationController.stop();
renderResult(scanData.code!);
}
});
final Barcode? barcode = capture.barcodes.firstOrNull;
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
isProcessingResult.value = true;
scannerController.stop();
animationController.stop();
print("相机识别到的内容: ${barcode.rawValue!}");
renderResult(barcode.rawValue!);
}
}
/// 恢复扫描状态
void resumeScanner() {
result.value = null;
qrViewController?.resumeCamera();
isProcessingResult.value = false;
try {
scannerController.start();
} catch (e) {
print("无法重启相机: $e");
}
animationController.repeat(reverse: false);
}
/// 从相册选择图片并扫描二维码
void scanFromGallery() async {
try {
final XFile? imageFile = await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) return; // 用户取消了选择
qrViewController?.pauseCamera();
animationController.stop();
String? scanResult;
try {
final image = img.decodeImage(await File(imageFile.path).readAsBytes());
if (image != null) {
//扫描图片
final pixels = Int32List.fromList(
image.map((pixel) {
return (pixel.a.toInt() << 24) |
(pixel.r.toInt() << 16) |
(pixel.g.toInt() << 8) |
pixel.b.toInt();
}).toList(),
);
final source = RGBLuminanceSource(image.width, image.height, pixels);
final bitmap = BinaryBitmap(HybridBinarizer(source));
final reader = QRCodeReader();
final result = reader.decode(bitmap);
scanResult = result.text;
}
} on NotFoundException {
scanResult = null;
} catch (e) {
//异常
scanResult = null;
final XFile? imageFile =
await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) {
return;
}
if (scanResult != null) {
scannerController.stop();
animationController.stop();
showLoading("正在识别...");
final BarcodeCapture? capture =
await scannerController.analyzeImage(imageFile.path);
dismissLoading();
final Barcode? firstBarcode = capture?.barcodes.firstOrNull;
if (firstBarcode?.rawValue != null &&
firstBarcode!.rawValue!.isNotEmpty) {
final String scanResult = firstBarcode.rawValue!;
print("相册识别到的内容: $scanResult");
renderResult(scanResult);
} else {
showErrorToast('未识别到二维码');
resumeScanner();
}
} catch (e) {
showErrorToast('从相册选择失败');
} catch (e, stackTrace) {
dismissLoading();
showErrorToast('从相册选择失败,请稍后重试');
print("scanFromGallery Error: $e\n$stackTrace");
resumeScanner();
}
}
/// 切换闪光灯
void toggleFlash() async {
await qrViewController?.toggleFlash();
isFlashOn.value = (await qrViewController?.getFlashStatus()) ?? false;
try {
await scannerController.toggleTorch();
final currentTorchState = scannerController.value.torchState;
isFlashOn.value = currentTorchState == TorchState.on;
} catch (e) {
print("切换闪光灯失败: $e");
showErrorToast("无法打开闪光灯");
}
}
/// 翻转相机
void flipCamera() async {
await qrViewController?.flipCamera();
await scannerController.switchCamera();
}
/// 请求相机权限
void requestPermission() async {
if (Platform.isIOS) {
var status = await Permission.camera.request();
if (status.isGranted) {
} else if (status.isPermanentlyDenied) {
openAppSettings();
} else {
showErrorToast('需要相机权限才能扫描二维码');
}
return;
}
final bool results = await requestCameraPermission();
if (!results) {
showErrorToast('相机权限未被授予,请到权限管理中打开');
var status = await Permission.camera.request();
if (!status.isGranted) {
showErrorToast('请授予相机权限以使用扫描功能');
Get.back();
}
}
void requestPhotoPermission() async {
if (Platform.isAndroid) {
final bool results = await requestPhotosPermission();
if (!results) {
showErrorToast('相册权限未被授予,请到权限管理中打开');
} else {
scanFromGallery();
}
}
if (Platform.isIOS) {
var status = await Permission.photos.request();
print("权限状态: $status"); // 在控制台看这个输出
if (status.isGranted) {
scanFromGallery();
} else if (status.isPermanentlyDenied) {
openAppSettings();
} else {
showErrorToast('需要相册权限才能从相册中选择图片');
}
var status = await Permission.photos.request();
if (status.isGranted) {
scanFromGallery();
} else if (status.isPermanentlyDenied) {
openAppSettings();
} else {
showErrorToast('需要相册权限才能从相册中选择图片');
}
}
//扫码结果处理 //如果绑定接口返回的data为null 需要手动编辑车牌
/// 处理扫描结果
void renderResult(String resultStr, {plateNumber}) async {
showLoading("正在获取车辆信息...");
try {
/*var responseData = await HttpService.to.get(
"appointment/truck/base-info?vin=$resultStr",
);*/
final Map<String, dynamic> requestData = {
"code": resultStr,
"phone": StorageService.to.phone,
@@ -184,6 +154,7 @@ class QrCodeController extends GetxController
);
if (responseData == null) {
dismissLoading();
showToast('无法获取车辆信息,请检查网络或稍后重试');
resumeScanner();
return;
@@ -192,62 +163,76 @@ class QrCodeController extends GetxController
if (result.code != 0) {
showToast(result.error);
dismissLoading();
resumeScanner(); // 绑定失败也要恢复扫描
return;
}
if (result.data == null) {
dismissLoading();
showBindDialog(resultStr);
return;
}
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
//保存使用
await StorageService.to.saveVehicleInfo(vehicle);
dismissLoading();
Get.back(result: true);
} on DioException catch (e) {
} on DioException catch (_) {
showErrorToast("网络请求失败,请稍后重试");
resumeScanner();
} catch (e, stackTrace) {
showErrorToast("处理失败,请稍后重");
resumeScanner(); // 未知异常,恢复扫描
} catch (e, _) {
showErrorToast("处理失败,请稍后重");
resumeScanner();
} finally {
dismissLoading();
if (Get.isDialogOpen ?? false) {
dismissLoading();
}
}
}
/// 显示绑定确认对话框
void showBindDialog(String resultStr) {
final TextEditingController plateNumberController = TextEditingController();
DialogX.to.showNoticeDialog(
icon: DialogIcon.info,
// 使用 showConfirmDialog它有 onCancel 回调
DialogX.to.showConfirmDialog(
title: '请输入车牌号',
barrierDismissible: false,
content: TextField(
controller: plateNumberController, // 绑定 controller
autofocus: false, // 弹窗出现时自动获取焦点,方便用户直接输入
controller: plateNumberController,
autofocus: false,
decoration: const InputDecoration(
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
),
),
confirmText: '确认',
confirmText: '确认绑定',
cancelText: '取消', // showConfirmDialog 有 cancelText
onConfirm: () {
final String plateNumber = plateNumberController.text.trim();
if (plateNumber.isEmpty) {
resumeScanner();
showToast("请输入车牌号");
return;
// 返回 false 可以阻止弹窗关闭,让用户继续输入
return false;
}
renderResult(resultStr, plateNumber: plateNumber);
//关闭弹窗
return true;
},
onCancel: () {
// 如果用户点击取消,恢复扫描
resumeScanner();
},
);
}
@override
void onClose() {
qrViewController?.dispose();
scannerController.dispose();
animationController.dispose();
super.onClose();
}
}