样式调整
This commit is contained in:
@@ -281,6 +281,8 @@
|
||||
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) {
|
||||
|
||||
@@ -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<void> _msgNotice() async {
|
||||
Future<void> msgNotice() async {
|
||||
final Map<String, dynamic> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,12 @@ class ReservationPage extends GetView<ReservationController> {
|
||||
),
|
||||
),
|
||||
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],
|
||||
|
||||
@@ -180,8 +180,12 @@ class SitePage extends GetView<SiteController> {
|
||||
),
|
||||
),
|
||||
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],
|
||||
|
||||
@@ -34,10 +34,10 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
getUserBindCarInfo();
|
||||
_msgNotice();
|
||||
msgNotice();
|
||||
}
|
||||
|
||||
Future<void> _msgNotice() async {
|
||||
Future<void> msgNotice() async {
|
||||
final Map<String, dynamic> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,8 +133,11 @@ class CarInfoPage extends GetView<CarInfoController> {
|
||||
),
|
||||
),
|
||||
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<CarInfoController> {
|
||||
),
|
||||
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<CarInfoController> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: true),
|
||||
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
|
||||
_buildCertDetailItem('运营城市', controller.address),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -140,8 +140,12 @@ class MinePage extends GetView<MineController> {
|
||||
),
|
||||
),
|
||||
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],
|
||||
|
||||
@@ -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<void> _msgNotice() async {
|
||||
Future<void> msgNotice() async {
|
||||
final Map<String, dynamic> 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();
|
||||
|
||||
@@ -152,7 +152,13 @@ class ReservationPage extends GetView<C_ReservationController> {
|
||||
),
|
||||
),
|
||||
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<C_ReservationController> {
|
||||
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<C_ReservationController> {
|
||||
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),
|
||||
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<C_ReservationController> {
|
||||
controller.amountController.text = newAmount.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
//更新进度条
|
||||
controller.current = newAmount;
|
||||
|
||||
// 移动光标到末尾,防止光标跳到前面
|
||||
controller.amountController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: controller.amountController.text.length),
|
||||
|
||||
@@ -532,7 +532,16 @@ class _LoginPageState extends State<LoginPage> 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() {
|
||||
|
||||
@@ -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<double> 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<double>(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;
|
||||
|
||||
// 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 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,
|
||||
/// 手动输入车牌绑定
|
||||
void manualInputBind(String plateNumber, int source) {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: plateNumber.toUpperCase() ?? '',
|
||||
);
|
||||
|
||||
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,
|
||||
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(),
|
||||
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;
|
||||
},
|
||||
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<String?> 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<String?> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<QrCodeController> {
|
||||
const QrCodePage({super.key});
|
||||
const QrCodePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(QrCodeController());
|
||||
return GetBuilder<QrCodeController>(
|
||||
init: QrCodeController(),
|
||||
id: 'qrcode',
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
title: const Text('扫码', style: TextStyle(color: Colors.white)),
|
||||
title: const Text('绑定车辆'),
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: SingleChildScrollView(child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.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,
|
||||
const SizedBox(height: 24),
|
||||
Icon(
|
||||
Icons.directions_car_rounded,
|
||||
size: 100,
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
),
|
||||
width: 250,
|
||||
height: 250,
|
||||
),
|
||||
),
|
||||
// 扫描动画和覆盖层
|
||||
_buildScannerOverlay(context),
|
||||
// 底部的功能按钮
|
||||
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 SizedBox(height: 24),
|
||||
const Text(
|
||||
'需要相机权限',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
||||
"请选择绑定方式",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'请授予相机权限以使用扫码功能。',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
"您可以拍摄照片自动识别,\n或手动输入车牌号进行绑定。",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: controller.requestPermission, // 点击按钮重新请求权限
|
||||
child: const Text('授予权限'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
const SizedBox(height: 60),
|
||||
|
||||
/// 构建扫描区域的覆盖层和动画
|
||||
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),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 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),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 100), // 底部留白
|
||||
],
|
||||
),
|
||||
),
|
||||
// 扫描动画
|
||||
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 _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: <Widget>[
|
||||
// 闪光灯按钮
|
||||
_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,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 翻转相机按钮
|
||||
_buildIconButton(
|
||||
onPressed: controller.flipCamera,
|
||||
child: const Icon(
|
||||
Icons.flip_camera_ios,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user