239 lines
6.9 KiB
Dart
239 lines
6.9 KiB
Dart
import 'dart:io';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:getx_scaffold/getx_scaffold.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/vehicle_info.dart';
|
||
import 'package:ln_jq_app/storage_service.dart';
|
||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||
import 'package:permission_handler/permission_handler.dart';
|
||
|
||
class QrCodeController extends GetxController
|
||
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
|
||
@override
|
||
String get builderId => 'qrcode';
|
||
|
||
// --- Animation ---
|
||
late final AnimationController animationController;
|
||
late final Animation<double> scanAnimation;
|
||
|
||
// --- 使用 MobileScanner 的控制器 ---
|
||
final MobileScannerController scannerController = MobileScannerController();
|
||
|
||
final RxBool isFlashOn = false.obs;
|
||
final RxBool isProcessingResult = false.obs;
|
||
|
||
@override
|
||
void onInit() {
|
||
super.onInit();
|
||
requestPermission();
|
||
|
||
animationController = AnimationController(
|
||
duration: const Duration(milliseconds: 2500),
|
||
vsync: this,
|
||
);
|
||
scanAnimation =
|
||
Tween<double>(begin: 0, end: 1).animate(animationController);
|
||
animationController.repeat(reverse: false);
|
||
}
|
||
|
||
/// MobileScanner 的 onDetect 回调方法
|
||
void onDetect(BarcodeCapture capture) {
|
||
if (isProcessingResult.value) return;
|
||
|
||
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() {
|
||
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;
|
||
}
|
||
|
||
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, stackTrace) {
|
||
dismissLoading();
|
||
showErrorToast('从相册选择失败,请稍后重试');
|
||
print("scanFromGallery Error: $e\n$stackTrace");
|
||
resumeScanner();
|
||
}
|
||
}
|
||
|
||
/// 切换闪光灯
|
||
void toggleFlash() async {
|
||
try {
|
||
await scannerController.toggleTorch();
|
||
final currentTorchState = scannerController.value.torchState;
|
||
isFlashOn.value = currentTorchState == TorchState.on;
|
||
} catch (e) {
|
||
print("切换闪光灯失败: $e");
|
||
showErrorToast("无法打开闪光灯");
|
||
}
|
||
}
|
||
|
||
/// 翻转相机
|
||
void flipCamera() async {
|
||
await scannerController.switchCamera();
|
||
}
|
||
|
||
/// 请求相机权限
|
||
void requestPermission() async {
|
||
var status = await Permission.camera.request();
|
||
if (!status.isGranted) {
|
||
showErrorToast('请授予相机权限以使用扫描功能');
|
||
Get.back();
|
||
}
|
||
}
|
||
|
||
void requestPhotoPermission() async {
|
||
var status = await Permission.photos.request();
|
||
if (status.isGranted) {
|
||
scanFromGallery();
|
||
} else if (status.isPermanentlyDenied) {
|
||
openAppSettings();
|
||
} else {
|
||
showErrorToast('需要相册权限才能从相册中选择图片');
|
||
}
|
||
}
|
||
|
||
/// 处理扫描结果
|
||
void renderResult(String resultStr, {plateNumber}) async {
|
||
showLoading("正在获取车辆信息...");
|
||
try {
|
||
final Map<String, dynamic> requestData = {
|
||
"code": resultStr,
|
||
"phone": StorageService.to.phone,
|
||
};
|
||
if (plateNumber != null && plateNumber.isNotEmpty) {
|
||
requestData['plateNumber'] = plateNumber;
|
||
}
|
||
var responseData = await HttpService.to.post(
|
||
"appointment/truck/bindTruck",
|
||
data: requestData,
|
||
);
|
||
|
||
if (responseData == null) {
|
||
dismissLoading();
|
||
showToast('无法获取车辆信息,请检查网络或稍后重试');
|
||
resumeScanner();
|
||
return;
|
||
}
|
||
var result = BaseModel.fromJson(responseData.data);
|
||
|
||
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 (_) {
|
||
showErrorToast("网络请求失败,请稍后重试");
|
||
resumeScanner();
|
||
} catch (e, _) {
|
||
showErrorToast("处理失败,请稍后重舍");
|
||
resumeScanner();
|
||
} finally {
|
||
if (Get.isDialogOpen ?? false) {
|
||
dismissLoading();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 显示绑定确认对话框
|
||
void showBindDialog(String resultStr) {
|
||
final TextEditingController plateNumberController = TextEditingController();
|
||
// 使用 showConfirmDialog,它有 onCancel 回调
|
||
DialogX.to.showConfirmDialog(
|
||
title: '请输入车牌号',
|
||
barrierDismissible: false,
|
||
content: TextField(
|
||
controller: plateNumberController,
|
||
autofocus: false,
|
||
decoration: const InputDecoration(
|
||
hintText: '请输入完整的车牌号',
|
||
border: OutlineInputBorder(),
|
||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
|
||
),
|
||
),
|
||
confirmText: '确认绑定',
|
||
cancelText: '取消', // showConfirmDialog 有 cancelText
|
||
onConfirm: () {
|
||
final String plateNumber = plateNumberController.text.trim();
|
||
if (plateNumber.isEmpty) {
|
||
showToast("请输入车牌号");
|
||
// 返回 false 可以阻止弹窗关闭,让用户继续输入
|
||
return false;
|
||
}
|
||
renderResult(resultStr, plateNumber: plateNumber);
|
||
//关闭弹窗
|
||
return true;
|
||
},
|
||
onCancel: () {
|
||
// 如果用户点击取消,恢复扫描
|
||
resumeScanner();
|
||
},
|
||
);
|
||
}
|
||
|
||
@override
|
||
void onClose() {
|
||
scannerController.dispose();
|
||
animationController.dispose();
|
||
super.onClose();
|
||
}
|
||
}
|
||
|