v1.2.1 待发布
This commit is contained in:
@@ -20,7 +20,9 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- MTBBarcodeScanner (5.0.11)
|
- mobile_scanner (7.0.0):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- OrderedSet (6.0.3)
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -29,9 +31,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- qr_code_scanner_plus (0.2.6):
|
|
||||||
- Flutter
|
|
||||||
- MTBBarcodeScanner
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -47,16 +46,15 @@ DEPENDENCIES:
|
|||||||
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`)
|
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- MTBBarcodeScanner
|
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@@ -76,14 +74,14 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
mobile_scanner:
|
||||||
|
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
qr_code_scanner_plus:
|
|
||||||
:path: ".symlinks/plugins/qr_code_scanner_plus/ios"
|
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
@@ -98,12 +96,11 @@ SPEC CHECKSUMS:
|
|||||||
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
|
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:image_picker/image_picker.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/model/vehicle_info.dart';
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
import 'package:zxing_lib/common.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:zxing_lib/qrcode.dart';
|
|
||||||
import 'package:zxing_lib/zxing.dart';
|
|
||||||
|
|
||||||
class QrCodeController extends GetxController
|
class QrCodeController extends GetxController
|
||||||
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
|
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
|
||||||
@@ -22,11 +18,11 @@ class QrCodeController extends GetxController
|
|||||||
late final AnimationController animationController;
|
late final AnimationController animationController;
|
||||||
late final Animation<double> scanAnimation;
|
late final Animation<double> scanAnimation;
|
||||||
|
|
||||||
// --- QR Scanning ---
|
// --- 使用 MobileScanner 的控制器 ---
|
||||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
final MobileScannerController scannerController = MobileScannerController();
|
||||||
QRViewController? qrViewController;
|
|
||||||
final Rx<Barcode?> result = Rx<Barcode?>(null);
|
|
||||||
final RxBool isFlashOn = false.obs;
|
final RxBool isFlashOn = false.obs;
|
||||||
|
final RxBool isProcessingResult = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@@ -37,140 +33,114 @@ class QrCodeController extends GetxController
|
|||||||
duration: const Duration(milliseconds: 2500),
|
duration: const Duration(milliseconds: 2500),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
scanAnimation = Tween<double>(begin: 0, end: 1).animate(animationController);
|
scanAnimation =
|
||||||
|
Tween<double>(begin: 0, end: 1).animate(animationController);
|
||||||
animationController.repeat(reverse: false);
|
animationController.repeat(reverse: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当 QRView 创建时调用
|
/// MobileScanner 的 onDetect 回调方法
|
||||||
void onQRViewCreated(QRViewController controller) {
|
void onDetect(BarcodeCapture capture) {
|
||||||
this.qrViewController = controller;
|
if (isProcessingResult.value) return;
|
||||||
// 监听扫描到的数据
|
|
||||||
controller.scannedDataStream.listen((scanData) {
|
|
||||||
if (scanData.code != null && result.value?.code != scanData.code) {
|
|
||||||
result.value = scanData;
|
|
||||||
qrViewController?.pauseCamera();
|
|
||||||
|
|
||||||
animationController.stop();
|
final Barcode? barcode = capture.barcodes.firstOrNull;
|
||||||
|
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
|
||||||
renderResult(scanData.code!);
|
isProcessingResult.value = true;
|
||||||
}
|
scannerController.stop();
|
||||||
});
|
animationController.stop();
|
||||||
|
print("相机识别到的内容: ${barcode.rawValue!}");
|
||||||
|
renderResult(barcode.rawValue!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 恢复扫描状态
|
||||||
void resumeScanner() {
|
void resumeScanner() {
|
||||||
result.value = null;
|
isProcessingResult.value = false;
|
||||||
qrViewController?.resumeCamera();
|
try {
|
||||||
|
scannerController.start();
|
||||||
|
} catch (e) {
|
||||||
|
print("无法重启相机: $e");
|
||||||
|
}
|
||||||
animationController.repeat(reverse: false);
|
animationController.repeat(reverse: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从相册选择图片并扫描二维码
|
/// 从相册选择图片并扫描二维码
|
||||||
void scanFromGallery() async {
|
void scanFromGallery() async {
|
||||||
try {
|
try {
|
||||||
final XFile? imageFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
final XFile? imageFile =
|
||||||
if (imageFile == null) return; // 用户取消了选择
|
await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
|
if (imageFile == null) {
|
||||||
qrViewController?.pauseCamera();
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
renderResult(scanResult);
|
||||||
} else {
|
} else {
|
||||||
showErrorToast('未识别到二维码');
|
showErrorToast('未识别到二维码');
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
showErrorToast('从相册选择失败');
|
dismissLoading();
|
||||||
|
showErrorToast('从相册选择失败,请稍后重试');
|
||||||
|
print("scanFromGallery Error: $e\n$stackTrace");
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换闪光灯
|
/// 切换闪光灯
|
||||||
void toggleFlash() async {
|
void toggleFlash() async {
|
||||||
await qrViewController?.toggleFlash();
|
try {
|
||||||
isFlashOn.value = (await qrViewController?.getFlashStatus()) ?? false;
|
await scannerController.toggleTorch();
|
||||||
|
final currentTorchState = scannerController.value.torchState;
|
||||||
|
isFlashOn.value = currentTorchState == TorchState.on;
|
||||||
|
} catch (e) {
|
||||||
|
print("切换闪光灯失败: $e");
|
||||||
|
showErrorToast("无法打开闪光灯");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 翻转相机
|
/// 翻转相机
|
||||||
void flipCamera() async {
|
void flipCamera() async {
|
||||||
await qrViewController?.flipCamera();
|
await scannerController.switchCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 请求相机权限
|
||||||
void requestPermission() async {
|
void requestPermission() async {
|
||||||
if (Platform.isIOS) {
|
var status = await Permission.camera.request();
|
||||||
var status = await Permission.camera.request();
|
if (!status.isGranted) {
|
||||||
if (status.isGranted) {
|
showErrorToast('请授予相机权限以使用扫描功能');
|
||||||
} else if (status.isPermanentlyDenied) {
|
Get.back();
|
||||||
openAppSettings();
|
|
||||||
} else {
|
|
||||||
showErrorToast('需要相机权限才能扫描二维码');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bool results = await requestCameraPermission();
|
|
||||||
if (!results) {
|
|
||||||
showErrorToast('相机权限未被授予,请到权限管理中打开');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestPhotoPermission() async {
|
void requestPhotoPermission() async {
|
||||||
if (Platform.isAndroid) {
|
var status = await Permission.photos.request();
|
||||||
final bool results = await requestPhotosPermission();
|
if (status.isGranted) {
|
||||||
if (!results) {
|
scanFromGallery();
|
||||||
showErrorToast('相册权限未被授予,请到权限管理中打开');
|
} else if (status.isPermanentlyDenied) {
|
||||||
} else {
|
openAppSettings();
|
||||||
scanFromGallery();
|
} else {
|
||||||
}
|
showErrorToast('需要相册权限才能从相册中选择图片');
|
||||||
}
|
|
||||||
if (Platform.isIOS) {
|
|
||||||
var status = await Permission.photos.request();
|
|
||||||
print("权限状态: $status"); // 在控制台看这个输出
|
|
||||||
if (status.isGranted) {
|
|
||||||
scanFromGallery();
|
|
||||||
} else if (status.isPermanentlyDenied) {
|
|
||||||
openAppSettings();
|
|
||||||
} else {
|
|
||||||
showErrorToast('需要相册权限才能从相册中选择图片');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//扫码结果处理 //如果绑定接口返回的data为null 需要手动编辑车牌
|
/// 处理扫描结果
|
||||||
void renderResult(String resultStr, {plateNumber}) async {
|
void renderResult(String resultStr, {plateNumber}) async {
|
||||||
showLoading("正在获取车辆信息...");
|
showLoading("正在获取车辆信息...");
|
||||||
try {
|
try {
|
||||||
/*var responseData = await HttpService.to.get(
|
|
||||||
"appointment/truck/base-info?vin=$resultStr",
|
|
||||||
);*/
|
|
||||||
|
|
||||||
final Map<String, dynamic> requestData = {
|
final Map<String, dynamic> requestData = {
|
||||||
"code": resultStr,
|
"code": resultStr,
|
||||||
"phone": StorageService.to.phone,
|
"phone": StorageService.to.phone,
|
||||||
@@ -184,6 +154,7 @@ class QrCodeController extends GetxController
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (responseData == null) {
|
if (responseData == null) {
|
||||||
|
dismissLoading();
|
||||||
showToast('无法获取车辆信息,请检查网络或稍后重试');
|
showToast('无法获取车辆信息,请检查网络或稍后重试');
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
return;
|
return;
|
||||||
@@ -192,62 +163,76 @@ class QrCodeController extends GetxController
|
|||||||
|
|
||||||
if (result.code != 0) {
|
if (result.code != 0) {
|
||||||
showToast(result.error);
|
showToast(result.error);
|
||||||
|
dismissLoading();
|
||||||
|
resumeScanner(); // 绑定失败也要恢复扫描
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.data == null) {
|
if (result.data == null) {
|
||||||
|
dismissLoading();
|
||||||
showBindDialog(resultStr);
|
showBindDialog(resultStr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
|
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
|
||||||
//保存使用
|
|
||||||
await StorageService.to.saveVehicleInfo(vehicle);
|
await StorageService.to.saveVehicleInfo(vehicle);
|
||||||
|
dismissLoading();
|
||||||
Get.back(result: true);
|
Get.back(result: true);
|
||||||
} on DioException catch (e) {
|
|
||||||
|
} on DioException catch (_) {
|
||||||
showErrorToast("网络请求失败,请稍后重试");
|
showErrorToast("网络请求失败,请稍后重试");
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, _) {
|
||||||
showErrorToast("处理失败,请稍后重试");
|
showErrorToast("处理失败,请稍后重舍");
|
||||||
resumeScanner(); // 未知异常,恢复扫描
|
resumeScanner();
|
||||||
} finally {
|
} finally {
|
||||||
dismissLoading();
|
if (Get.isDialogOpen ?? false) {
|
||||||
|
dismissLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 显示绑定确认对话框
|
||||||
void showBindDialog(String resultStr) {
|
void showBindDialog(String resultStr) {
|
||||||
final TextEditingController plateNumberController = TextEditingController();
|
final TextEditingController plateNumberController = TextEditingController();
|
||||||
DialogX.to.showNoticeDialog(
|
// 使用 showConfirmDialog,它有 onCancel 回调
|
||||||
icon: DialogIcon.info,
|
DialogX.to.showConfirmDialog(
|
||||||
title: '请输入车牌号',
|
title: '请输入车牌号',
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: plateNumberController, // 绑定 controller
|
controller: plateNumberController,
|
||||||
autofocus: false, // 弹窗出现时自动获取焦点,方便用户直接输入
|
autofocus: false,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: '请输入完整的车牌号',
|
hintText: '请输入完整的车牌号',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
|
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
confirmText: '确认',
|
confirmText: '确认绑定',
|
||||||
|
cancelText: '取消', // showConfirmDialog 有 cancelText
|
||||||
onConfirm: () {
|
onConfirm: () {
|
||||||
final String plateNumber = plateNumberController.text.trim();
|
final String plateNumber = plateNumberController.text.trim();
|
||||||
if (plateNumber.isEmpty) {
|
if (plateNumber.isEmpty) {
|
||||||
resumeScanner();
|
|
||||||
showToast("请输入车牌号");
|
showToast("请输入车牌号");
|
||||||
return;
|
// 返回 false 可以阻止弹窗关闭,让用户继续输入
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
renderResult(resultStr, plateNumber: plateNumber);
|
renderResult(resultStr, plateNumber: plateNumber);
|
||||||
|
//关闭弹窗
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
// 如果用户点击取消,恢复扫描
|
||||||
|
resumeScanner();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
qrViewController?.dispose();
|
scannerController.dispose();
|
||||||
animationController.dispose();
|
animationController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,148 +1,155 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
class QrCodePage extends GetView<QrCodeController> {
|
class QrCodePage extends GetView<QrCodeController> {
|
||||||
const QrCodePage({Key? key}) : super(key: key);
|
const QrCodePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<QrCodeController>(
|
Get.put(QrCodeController());
|
||||||
init: QrCodeController(),
|
return Scaffold(
|
||||||
id: 'qrcode',
|
extendBodyBehindAppBar: true,
|
||||||
builder: (_) {
|
appBar: AppBar(
|
||||||
return Scaffold(
|
title: const Text('扫码', style: TextStyle(color: Colors.white)),
|
||||||
extendBodyBehindAppBar: true,
|
centerTitle: true,
|
||||||
appBar: AppBar(
|
backgroundColor: Colors.transparent,
|
||||||
title: const Text('扫码', style: TextStyle(color: Colors.white)),
|
elevation: 0,
|
||||||
centerTitle: true,
|
leading: IconButton(
|
||||||
backgroundColor: Colors.transparent,
|
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||||
elevation: 0,
|
onPressed: () => Get.back(),
|
||||||
leading: IconButton(
|
),
|
||||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
actions: [
|
||||||
onPressed: () => Get.back(),
|
TextButton(
|
||||||
|
onPressed: controller.requestPhotoPermission,
|
||||||
|
child: const Text('相册', style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// 1. 使用 MobileScanner 作为扫描视图
|
||||||
|
MobileScanner(
|
||||||
|
controller: controller.scannerController,
|
||||||
|
onDetect: controller.onDetect,
|
||||||
|
// 您可以自定义扫描框的样式
|
||||||
|
scanWindow: Rect.fromCenter(
|
||||||
|
center: Offset(
|
||||||
|
MediaQuery.of(context).size.width / 2,
|
||||||
|
MediaQuery.of(context).size.height / 2 - 50,
|
||||||
|
),
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: controller.requestPhotoPermission,
|
|
||||||
child: const Text(
|
|
||||||
'相册',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: Stack(
|
// 扫描动画和覆盖层
|
||||||
children: <Widget>[
|
_buildScannerOverlay(context),
|
||||||
_buildQrView(context),
|
// 底部的功能按钮
|
||||||
Positioned(
|
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
|
||||||
bottom: 80.h,
|
],
|
||||||
left: 0,
|
),
|
||||||
right: 0,
|
|
||||||
child: _buildControlButtons(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建二维码扫描视图(带动画)
|
/// 构建扫描区域的覆盖层和动画
|
||||||
Widget _buildQrView(BuildContext context) {
|
Widget _buildScannerOverlay(BuildContext context) {
|
||||||
// 定义扫描区域的大小
|
// 模拟扫描框的位置和大小
|
||||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
const double scanAreaSize = 250.0;
|
||||||
MediaQuery.of(context).size.height < 400)
|
|
||||||
? 250.0
|
|
||||||
: 300.0;
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
children: [
|
||||||
children: <Widget>[
|
// 半透明的覆盖层
|
||||||
// 底层是相机视图和半透明遮罩
|
ColorFiltered(
|
||||||
QRView(
|
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.srcOut),
|
||||||
key: controller.qrKey,
|
child: Stack(
|
||||||
onQRViewCreated: controller.onQRViewCreated,
|
children: [
|
||||||
overlay: QrScannerOverlayShape(
|
Container(decoration: const BoxDecoration(color: Colors.transparent)),
|
||||||
borderColor: Colors.blueAccent,
|
Align(
|
||||||
borderRadius: 10,
|
alignment: Alignment.center,
|
||||||
borderLength: 30,
|
|
||||||
borderWidth: 10,
|
|
||||||
cutOutSize: scanArea,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 上层是扫描动画
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: controller.scanAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Positioned(
|
|
||||||
// 计算扫描框的顶部位置,以便动画从顶部开始
|
|
||||||
top: (MediaQuery.of(context).size.height - scanArea) / 2,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0, controller.scanAnimation.value * scanArea),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: scanArea,
|
margin: const EdgeInsets.only(bottom: 100), // 微调位置
|
||||||
height: 2, // 扫描线的高度
|
width: scanAreaSize,
|
||||||
|
height: scanAreaSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blueAccent,
|
color: Colors.black,
|
||||||
boxShadow: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blueAccent.withOpacity(0.7),
|
|
||||||
blurRadius: 8,
|
|
||||||
spreadRadius: 2,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
|
),
|
||||||
|
// 扫描动画
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 100),
|
||||||
|
width: scanAreaSize,
|
||||||
|
height: scanAreaSize,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: controller.scanAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: ScannerAnimationPainter(
|
||||||
|
controller.scanAnimation.value,
|
||||||
|
AppTheme.themeColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建底部的控制按钮
|
/// 构建底部的功能按钮(闪光灯、相册)
|
||||||
Widget _buildControlButtons() {
|
Widget _buildActionButtons() {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'将二维码/条形码放入框内,即可自动扫描',
|
'将二维码/条形码放入框内,即可自动扫描',
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// 闪光灯按钮
|
// 闪光灯按钮
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
onPressed: controller.toggleFlash,
|
onPressed: controller.toggleFlash,
|
||||||
//闪光灯状态的变化
|
//闪光灯状态的变化
|
||||||
child: Obx(() => Icon(
|
child: Obx(
|
||||||
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
|
() => IconButton(
|
||||||
color: Colors.white,
|
icon: Icon(
|
||||||
size: 28,
|
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
|
||||||
)),
|
color: Colors.white,
|
||||||
),
|
size: 28,
|
||||||
// 翻转相机按钮
|
),
|
||||||
_buildIconButton(
|
onPressed: controller.toggleFlash,
|
||||||
onPressed: controller.flipCamera,
|
),
|
||||||
child: const Icon(
|
),
|
||||||
Icons.flip_camera_ios,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 28,
|
|
||||||
),
|
),
|
||||||
),
|
// 翻转相机按钮
|
||||||
],
|
_buildIconButton(
|
||||||
),
|
onPressed: controller.flipCamera,
|
||||||
],
|
child: const Icon(
|
||||||
);
|
Icons.flip_camera_ios,
|
||||||
}
|
color: Colors.white,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
|
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -158,3 +165,71 @@ class QrCodePage extends GetView<QrCodeController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 扫描动画的绘制器
|
||||||
|
class ScannerAnimationPainter extends CustomPainter {
|
||||||
|
final double value;
|
||||||
|
final Color borderColor;
|
||||||
|
|
||||||
|
ScannerAnimationPainter(this.value, this.borderColor);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = borderColor
|
||||||
|
..strokeWidth = 3
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final cornerLength = 20.0;
|
||||||
|
// 绘制四个角的边框
|
||||||
|
// Top-left
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, cornerLength)
|
||||||
|
..lineTo(0, 0)
|
||||||
|
..lineTo(cornerLength, 0),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
// Top-right
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(size.width - cornerLength, 0)
|
||||||
|
..lineTo(size.width, 0)
|
||||||
|
..lineTo(size.width, cornerLength),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
// Bottom-left
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, size.height - cornerLength)
|
||||||
|
..lineTo(0, size.height)
|
||||||
|
..lineTo(cornerLength, size.height),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
// Bottom-right
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(size.width - cornerLength, size.height)
|
||||||
|
..lineTo(size.width, size.height)
|
||||||
|
..lineTo(size.width, size.height - cornerLength),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制扫描线
|
||||||
|
final linePaint = Paint()
|
||||||
|
..color = borderColor.withOpacity(0.8)
|
||||||
|
..strokeWidth = 2
|
||||||
|
..shader = LinearGradient(
|
||||||
|
colors: [borderColor.withOpacity(0), borderColor, borderColor.withOpacity(0)],
|
||||||
|
stops: const [0.0, 0.5, 1.0],
|
||||||
|
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||||
|
|
||||||
|
final y = size.height * value;
|
||||||
|
canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,14 +65,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
charset:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: charset
|
|
||||||
sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -733,6 +725,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
mobile_scanner:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mobile_scanner
|
||||||
|
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.4"
|
||||||
modal_bottom_sheet:
|
modal_bottom_sheet:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -925,14 +925,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.3"
|
||||||
qr_code_scanner_plus:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: qr_code_scanner_plus
|
|
||||||
sha256: b764e5004251c58d9dee0c295e6006e05bd8d249e78ac3383abdb5afe0a996cd
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.14"
|
|
||||||
rational:
|
rational:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1234,14 +1226,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
zxing_lib:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: zxing_lib
|
|
||||||
sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.4"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ dependencies:
|
|||||||
|
|
||||||
flutter_native_splash: ^2.4.7
|
flutter_native_splash: ^2.4.7
|
||||||
dropdown_button2: ^2.3.8
|
dropdown_button2: ^2.3.8
|
||||||
qr_code_scanner_plus: ^2.0.14
|
|
||||||
image_picker: ^1.2.1 # 用于从相册选择图片
|
image_picker: ^1.2.1 # 用于从相册选择图片
|
||||||
image: ^4.5.4
|
image: ^4.5.4
|
||||||
zxing_lib: ^1.1.4
|
mobile_scanner: ^7.1.4
|
||||||
flutter_pdfview: 1.4.3 #显示pdf
|
flutter_pdfview: 1.4.3 #显示pdf
|
||||||
photo_view: ^0.15.0 #操作图片
|
photo_view: ^0.15.0 #操作图片
|
||||||
flutter_inappwebview: ^6.1.5 # WebView插件
|
flutter_inappwebview: ^6.1.5 # WebView插件
|
||||||
|
|||||||
Reference in New Issue
Block a user