From 1177be821a040686b6232e310e3a4d35420a1f0c Mon Sep 17 00:00:00 2001 From: userGyl Date: Thu, 29 Jan 2026 17:01:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/assets/html/map.html | 9 +- .../pages/b_page/reservation/controller.dart | 5 +- .../lib/pages/b_page/reservation/view.dart | 8 +- ln_jq_app/lib/pages/b_page/site/view.dart | 8 +- .../lib/pages/c_page/car_info/controller.dart | 5 +- ln_jq_app/lib/pages/c_page/car_info/view.dart | 11 +- ln_jq_app/lib/pages/c_page/mine/view.dart | 8 +- .../pages/c_page/reservation/controller.dart | 16 +- .../lib/pages/c_page/reservation/view.dart | 25 +- ln_jq_app/lib/pages/login/view.dart | 11 +- ln_jq_app/lib/pages/qr_code/controller.dart | 328 +++++++----------- ln_jq_app/lib/pages/qr_code/view.dart | 327 ++++------------- 12 files changed, 260 insertions(+), 501 deletions(-) diff --git a/ln_jq_app/assets/html/map.html b/ln_jq_app/assets/html/map.html index 7e79fb2..a9dd60b 100644 --- a/ln_jq_app/assets/html/map.html +++ b/ln_jq_app/assets/html/map.html @@ -280,7 +280,9 @@ const regeo = result.regeocode; const addressComponent = regeo.addressComponent; const pois = regeo.pois; - + + console.log("地理:"+JSON.stringify(result)); + // 策略1: 优先使用最近的、类型合适的POI的名称 if (pois && pois.length > 0) { // 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名 @@ -295,8 +297,9 @@ } // 策略2: 如果没有POI,使用"道路+门牌号" else if (addressComponent.street && addressComponent.streetNumber) { - shortAddress = addressComponent.street + addressComponent - .streetNumber; + shortAddress = addressComponent.district+ + addressComponent.township+ + addressComponent.street + addressComponent.streetNumber; } // 策略3: 如果还没有,使用"区+乡镇" else if (addressComponent.district) { diff --git a/ln_jq_app/lib/pages/b_page/reservation/controller.dart b/ln_jq_app/lib/pages/b_page/reservation/controller.dart index e28000d..1c3b1a6 100644 --- a/ln_jq_app/lib/pages/b_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/b_page/reservation/controller.dart @@ -45,7 +45,7 @@ class ReservationController extends GetxController with BaseControllerMixin { customStartTime = DateTime.now(); customEndTime = customStartTime!.add(const Duration(days: 1)); renderData(); - _msgNotice(); // 红点消息 + msgNotice(); // 红点消息 startAutoRefresh(); } @@ -248,7 +248,7 @@ class ReservationController extends GetxController with BaseControllerMixin { } } - Future _msgNotice() async { + Future msgNotice() async { final Map requestData = { 'appFlag': 1, 'isRead': 1, @@ -264,6 +264,7 @@ class ReservationController extends GetxController with BaseControllerMixin { if (result.code == 0 && result.data != null) { String total = result.data["total"].toString(); isNotice = int.parse(total) > 0; + updateUi(); } } } diff --git a/ln_jq_app/lib/pages/b_page/reservation/view.dart b/ln_jq_app/lib/pages/b_page/reservation/view.dart index 0c9d803..8ea7204 100644 --- a/ln_jq_app/lib/pages/b_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/b_page/reservation/view.dart @@ -98,8 +98,12 @@ class ReservationPage extends GetView { ), ), IconButton( - onPressed: () { - Get.to(() => const MessagePage()); + onPressed: () async{ + var scanResult = await Get.to(() => const MessagePage()); + if (scanResult == null) { + controller.msgNotice(); + } + }, style: IconButton.styleFrom( backgroundColor: Colors.grey[100], diff --git a/ln_jq_app/lib/pages/b_page/site/view.dart b/ln_jq_app/lib/pages/b_page/site/view.dart index b220993..0ea212a 100644 --- a/ln_jq_app/lib/pages/b_page/site/view.dart +++ b/ln_jq_app/lib/pages/b_page/site/view.dart @@ -180,8 +180,12 @@ class SitePage extends GetView { ), ), IconButton( - onPressed: () { - Get.to(() => const MessagePage()); + onPressed: () async{ + var scanResult = await Get.to(() => const MessagePage()); + if (scanResult == null) { + controller.msgNotice(); + } + }, style: IconButton.styleFrom( backgroundColor: Colors.grey[100], diff --git a/ln_jq_app/lib/pages/c_page/car_info/controller.dart b/ln_jq_app/lib/pages/c_page/car_info/controller.dart index 27232ee..ee2045c 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/controller.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/controller.dart @@ -34,10 +34,10 @@ class CarInfoController extends GetxController with BaseControllerMixin { void onInit() { super.onInit(); getUserBindCarInfo(); - _msgNotice(); + msgNotice(); } - Future _msgNotice() async { + Future msgNotice() async { final Map requestData = { 'appFlag': 1, 'isRead': 1, @@ -53,6 +53,7 @@ class CarInfoController extends GetxController with BaseControllerMixin { if (result.code == 0 && result.data != null) { String total = result.data["total"].toString(); isNotice = int.parse(total) > 0; + updateUi(); } } } diff --git a/ln_jq_app/lib/pages/c_page/car_info/view.dart b/ln_jq_app/lib/pages/c_page/car_info/view.dart index 6d50d14..0811bc4 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/view.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/view.dart @@ -133,8 +133,11 @@ class CarInfoPage extends GetView { ), ), IconButton( - onPressed: () { - Get.to(() => const MessagePage()); + onPressed: () async{ + var scanResult = await Get.to(() => const MessagePage()); + if (scanResult == null) { + controller.msgNotice(); + } }, style: IconButton.styleFrom( backgroundColor: Colors.grey[100], @@ -345,7 +348,7 @@ class CarInfoPage extends GetView { ), const SizedBox(height: 9), SizedBox( - height: 336.h, // 给定一个高度,或者使用别的方式布局 + height: 356.h, // 给定一个高度,或者使用别的方式布局 child: TabBarView( children: [ _buildCertificateContent('行驶证', controller.drivingAttachments), @@ -379,7 +382,7 @@ class CarInfoPage extends GetView { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: true), + _buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false), _buildCertDetailItem('运营城市', controller.address), ], ), diff --git a/ln_jq_app/lib/pages/c_page/mine/view.dart b/ln_jq_app/lib/pages/c_page/mine/view.dart index 3e7ea1e..b816ca5 100644 --- a/ln_jq_app/lib/pages/c_page/mine/view.dart +++ b/ln_jq_app/lib/pages/c_page/mine/view.dart @@ -140,8 +140,12 @@ class MinePage extends GetView { ), ), IconButton( - onPressed: () { - Get.to(() => const MessagePage()); + onPressed: () async{ + var scanResult = await Get.to(() => const MessagePage()); + if (scanResult == null) { + controller.msgNotice(); + } + }, style: IconButton.styleFrom( backgroundColor: Colors.grey[100], diff --git a/ln_jq_app/lib/pages/c_page/reservation/controller.dart b/ln_jq_app/lib/pages/c_page/reservation/controller.dart index d95fb69..d3cd565 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -363,10 +363,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin { (s) => s.hydrogenId == selectedStationId.value, ); - if (selectedStation.siteStatusName != "营运中") { + /*if (selectedStation.siteStatusName != "营运中") { showToast("该站点${selectedStation.siteStatusName},暂无法预约"); return; - } + }*/ showLoading("提交中"); @@ -552,7 +552,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { getUserBindCarInfo(); getSiteList(); startAutoRefresh(); - _msgNotice(); + msgNotice(); if (!init) { _setupListener(); @@ -562,7 +562,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { bool isNotice = false; - Future _msgNotice() async { + Future msgNotice() async { final Map requestData = { 'appFlag': 1, 'isRead': 1, @@ -578,6 +578,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { if (result.code == 0 && result.data != null) { String total = result.data["total"].toString(); isNotice = int.parse(total) > 0; + updateUi(); } } } @@ -656,8 +657,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin { var result = BaseModel.fromJson(responseData.data); + final value = double.tryParse( + result.data["fillingWeight"]?.toString() ?? '0', + ) ?? 0; + final String formatted = value.toStringAsFixed(2); + fillingWeight = - "${result.data["fillingWeight"]}${result.data["fillingWeightUnit"]}"; + "$formatted${result.data["fillingWeightUnit"]}"; fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}"; updateUi(); diff --git a/ln_jq_app/lib/pages/c_page/reservation/view.dart b/ln_jq_app/lib/pages/c_page/reservation/view.dart index 47a22c4..0a4f5d5 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/view.dart @@ -152,7 +152,13 @@ class ReservationPage extends GetView { ), ), IconButton( - onPressed: () => Get.to(() => const MessagePage()), + onPressed: () async{ + var scanResult = await Get.to(() => const MessagePage()); + if (scanResult == null) { + controller.msgNotice(); + } + + }, icon: Badge( smallSize: 8, backgroundColor: controller.isNotice @@ -519,7 +525,7 @@ class ReservationPage extends GetView { color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( - padding: const EdgeInsets.all(13.0), + padding: EdgeInsets.all(13.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -570,15 +576,19 @@ class ReservationPage extends GetView { children: [ IconButton( onPressed: () => _updateAmount(-1), - icon: const Icon(Icons.remove, size: 18, color: Colors.grey), + icon: Icon(Icons.remove, size: 14.sp, color: Colors.grey), ), Text( - "${controller.amountController.text} Kg", - style: const TextStyle(fontSize: 14, color: Colors.black87), + "${controller.amountController.text}Kg", + style: TextStyle( + fontSize: 11.sp, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), ), IconButton( onPressed: () => _updateAmount(1), - icon: const Icon(Icons.add, size: 18, color: Colors.grey), + icon: Icon(Icons.add, size: 14.sp, color: Colors.grey), ), ], ), @@ -758,6 +768,9 @@ class ReservationPage extends GetView { controller.amountController.text = newAmount.toStringAsFixed(2); } + //更新进度条 + controller.current = newAmount; + // 移动光标到末尾,防止光标跳到前面 controller.amountController.selection = TextSelection.fromPosition( TextPosition(offset: controller.amountController.text.length), diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index ac8f4b2..efaf5c4 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -532,7 +532,16 @@ class _LoginPageState extends State with SingleTickerProviderStateMix final _aliyunPush = AliyunPushFlutter(); void addAlias(String alias) async { - await _aliyunPush.addAlias(alias); + var result = await _aliyunPush.bindAccount(alias); + + var code = result['code']; + if (code == kAliyunPushSuccessCode) { + Logger.d('添加别名$alias成功'); + } else { + var errorCode = result['code']; + var errorMsg = result['errorMsg']; + Logger.d('添加别名$alias失败: $errorCode - $errorMsg'); + } } Widget buildAgreement() { diff --git a/ln_jq_app/lib/pages/qr_code/controller.dart b/ln_jq_app/lib/pages/qr_code/controller.dart index b7ea3ff..261e530 100644 --- a/ln_jq_app/lib/pages/qr_code/controller.dart +++ b/ln_jq_app/lib/pages/qr_code/controller.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:dio/dio.dart' as dio; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; @@ -6,243 +7,156 @@ 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 { +class QrCodeController extends GetxController with BaseControllerMixin { @override String get builderId => 'qrcode'; - // --- Animation --- - late final AnimationController animationController; - late final Animation scanAnimation; - - // --- 使用 MobileScanner 的控制器 --- - final MobileScannerController scannerController = MobileScannerController(); - - final RxBool isFlashOn = false.obs; - final RxBool isProcessingResult = false.obs; - - final RxBool hasPermission = false.obs; + final RxBool hasCameraPermission = false.obs; @override void onInit() { super.onInit(); - requestPermission(); - - animationController = AnimationController( - duration: const Duration(milliseconds: 2500), - vsync: this, - ); - scanAnimation = - Tween(begin: 0, end: 1).animate(animationController); - animationController.repeat(reverse: false); + _checkPermission(); } - /// 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 _checkPermission() async { + var status = await Permission.camera.status; + hasCameraPermission.value = status.isGranted; } - /// 恢复扫描状态 - 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 { + /// 1. 拍照并识别车牌流程 + void takePhotoAndRecognize() 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('请授予相机权限以使用扫描功能'); - } + if (status.isPermanentlyDenied) openAppSettings(); + showErrorToast("需要相机权限才能拍照识别"); + return; } - } + final XFile? photo = await ImagePicker().pickImage( + source: ImageSource.camera, + imageQuality: 80, // 压缩图片质量以加快上传 + ); + if (photo == null) return; - 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 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); - 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(); + // 1.1 上传文件 + String? imageUrl = await uploadFile(photo.path); + if (imageUrl != null) { + // 1.2 获取车牌号 + String? plateNumber = await getPlateNumber(imageUrl); + if (plateNumber != null) { + // 1.3 弹窗确认 + manualInputBind(plateNumber, 0); } } } - /// 显示绑定确认对话框 - void showBindDialog(String resultStr) { - final TextEditingController plateNumberController = TextEditingController(); - // 使用 showConfirmDialog,它有 onCancel 回调 + /// 手动输入车牌绑定 + void manualInputBind(String plateNumber, int source) { + final TextEditingController controller = TextEditingController( + text: plateNumber.toUpperCase() ?? '', + ); + DialogX.to.showConfirmDialog( - title: '请输入车牌号', - barrierDismissible: false, - content: TextField( - controller: plateNumberController, - autofocus: false, - decoration: const InputDecoration( - hintText: '请输入完整的车牌号', - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12.0), + title: source == 0 ? "识别结果" : '手动输入车牌', + content: SizedBox( + height: 40.h, + child: TextField( + textAlign: TextAlign.start, + controller: controller, + autofocus: plateNumber.isEmpty, + textCapitalization: TextCapitalization.characters, + decoration: const InputDecoration( + contentPadding: EdgeInsets.only(left: 5), + hintText: '请输入完整的车牌号', + border: OutlineInputBorder(), + ), ), ), confirmText: '确认绑定', - cancelText: '取消', // showConfirmDialog 有 cancelText - onConfirm: () { - final String plateNumber = plateNumberController.text.trim(); - if (plateNumber.isEmpty) { - showToast("请输入车牌号"); - // 返回 false 可以阻止弹窗关闭,让用户继续输入 - return false; - } - renderResult(resultStr, plateNumber: plateNumber); - //关闭弹窗 - return true; - }, + cancelText: source == 0 ? "重新拍摄" : '取消', onCancel: () { - // 如果用户点击取消,恢复扫描 - resumeScanner(); + if (source == 0) { + takePhotoAndRecognize(); + } + }, + onConfirm: () { + final plate = controller.text.trim().toUpperCase(); + if (plate.isNotEmpty) { + bindVehicleByPlate(plate); + } }, ); } - @override - void onClose() { - scannerController.dispose(); - animationController.dispose(); - super.onClose(); + /// 上传图片 + Future 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 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; + } + + /// 核心绑定方法 + void bindVehicleByPlate(String plateNumber) async { + showLoading("正在绑定车辆..."); + try { + var responseData = await HttpService.to.post( + "appointment/truck/bindOcrTruck", + data: {"plateNumber": plateNumber, "phone": StorageService.to.phone}, + ); + + var result = BaseModel.fromJson(responseData?.data); + if (result.code == 0 && result.data != null) { + await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(result.data)); + dismissLoading(); + showSuccessToast("绑定成功"); + Get.back(result: true); + } else { + showErrorToast(result.error); + } + } catch (e) { + showErrorToast("绑定失败,请检查网络"); + } finally { + dismissLoading(); + } } } - diff --git a/ln_jq_app/lib/pages/qr_code/view.dart b/ln_jq_app/lib/pages/qr_code/view.dart index 1dd37d0..f55a51d 100644 --- a/ln_jq_app/lib/pages/qr_code/view.dart +++ b/ln_jq_app/lib/pages/qr_code/view.dart @@ -1,285 +1,82 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:ln_jq_app/common/styles/theme.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:getx_scaffold/getx_scaffold.dart'; import 'controller.dart'; class QrCodePage extends GetView { - const QrCodePage({super.key}); + const QrCodePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - Get.put(QrCodeController()); - return Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - title: const Text('扫码', style: TextStyle(color: Colors.white)), - centerTitle: true, - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.white), - onPressed: () => Get.back(), - ), - actions: [ - TextButton( - onPressed: controller.requestPhotoPermission, - child: const Text('相册', style: TextStyle(color: Colors.white, fontSize: 16)), + return GetBuilder( + init: QrCodeController(), + id: 'qrcode', + builder: (controller) { + return Scaffold( + appBar: AppBar( + title: const Text('绑定车辆'), + centerTitle: true, + elevation: 0, ), - ], - ), - body: Obx(() { // 1. 使用 Obx 包裹整个 body - // 根据权限状态来决定显示什么 - if (controller.hasPermission.value) { - // 如果有权限,显示扫描器 - return _buildScannerView(context); - } else { - // 如果没有权限,显示引导界面 - return _buildPermissionDeniedView(); - } - }), - ); - } - Widget _buildScannerView(BuildContext context){ - if (!controller.animationController.isAnimating) { - controller.animationController.repeat(reverse: false); - } - return Stack( - alignment: Alignment.center, - children: [ - // 使用 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, - ), - ), - // 扫描动画和覆盖层 - _buildScannerOverlay(context), - // 底部的功能按钮 - Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()), - ], - ); - } + body: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: SingleChildScrollView(child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 24), + Icon( + Icons.directions_car_rounded, + size: 100, + color: Theme.of(context).primaryColor.withOpacity(0.1), + ), + const SizedBox(height: 24), + const Text( + "请选择绑定方式", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "您可以拍摄照片自动识别,\n或手动输入车牌号进行绑定。", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + const SizedBox(height: 60), - Widget _buildPermissionDeniedView() { - // 确保动画是停止的 - if (controller.animationController.isAnimating) { - controller.animationController.stop(); - } - - return Container( - color: Colors.black, - alignment: Alignment.center, - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.no_photography, color: Colors.white70, size: 64), - const SizedBox(height: 16), - const Text( - '需要相机权限', - style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - '请授予相机权限以使用扫码功能。', - style: TextStyle(color: Colors.white70, fontSize: 14), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: controller.requestPermission, // 点击按钮重新请求权限 - child: const Text('授予权限'), - ), - ], - ), - ); - } - - /// 构建扫描区域的覆盖层和动画 - Widget _buildScannerOverlay(BuildContext context) { - // 模拟扫描框的位置和大小 - const double scanAreaSize = 250.0; - return Stack( - children: [ - // 半透明的覆盖层 - ColorFiltered( - colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.srcOut), - child: Stack( - children: [ - Container(decoration: const BoxDecoration(color: Colors.transparent)), - Align( - alignment: Alignment.center, - child: Container( - margin: const EdgeInsets.only(bottom: 100), // 微调位置 - width: scanAreaSize, - height: scanAreaSize, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(12), + // 拍照识别按钮 + ElevatedButton.icon( + onPressed: controller.takePhotoAndRecognize, + icon: const Icon(Icons.camera_alt_rounded), + label: const Text("拍照识别车牌"), + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), - ), - ], - ), - ), - // 扫描动画 - 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, - ), - ); - }, - ), - ), - ), - ], - ); - } + const SizedBox(height: 20), - /// 构建底部的功能按钮(闪光灯、相册) - Widget _buildActionButtons() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - '将二维码/条形码放入框内,即可自动扫描', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 闪光灯按钮 - _buildIconButton( - onPressed: controller.toggleFlash, - //闪光灯状态的变化 - child: Obx( - () => IconButton( - icon: Icon( - controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off, - color: Colors.white, - size: 28, - ), - onPressed: controller.toggleFlash, + // 3. 手动输入按钮 + OutlinedButton.icon( + onPressed: (){ + controller.manualInputBind("",1); + }, + icon: const Icon(Icons.edit_note_rounded), + label: const Text("手动输入车牌"), + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + side: BorderSide(color: Theme.of(context).primaryColor, width: 1.5), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), - ), - // 翻转相机按钮 - _buildIconButton( - onPressed: controller.flipCamera, - child: const Icon( - Icons.flip_camera_ios, - color: Colors.white, - size: 28, - ), - ), - ], + const SizedBox(height: 100), // 底部留白 + ], + ),), ), - ], - ); - } - - - Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) { - return Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, - ), - child: IconButton( - onPressed: onPressed, - icon: child, - iconSize: 32, // 增大点击区域 - ), + ); + }, ); } } - -/// 扫描动画的绘制器 -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; - } -}