Files
ln-ios/ln_jq_app/lib/pages/qr_code/controller.dart

249 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
final RxBool hasPermission = 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();
renderResult(barcode.rawValue!);
}
}
/// 恢复扫描状态
void resumeScanner() {
isProcessingResult.value = false;
try {
scannerController.start();
animationController.repeat(reverse: false);
} catch (e) {
print("无法重启相机: $e");
}
}
/// 从相册选择图片并扫描二维码
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();
hasPermission.value = status.isGranted;
if (!status.isGranted) {
if (status.isPermanentlyDenied) {
showErrorToast('相机权限已被永久拒绝,请到系统设置中开启');
// 延迟一会再引导用户去设置
Future.delayed(const Duration(seconds: 2), () => openAppSettings());
} else {
showErrorToast('请授予相机权限以使用扫描功能');
}
}
}
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();
}
}