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;
- }
-}