From 9ce46a0c7d334a1f8b2f44e30d02bd649bff73b2 Mon Sep 17 00:00:00 2001 From: userGyl Date: Tue, 16 Dec 2025 11:58:00 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E5=8A=A0=E6=B0=A2=E7=AB=99=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E9=80=89=E6=8B=A9=EF=BC=8C=E6=BC=94=E7=A4=BA=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=95=B0=E6=8D=AE=20=E9=99=84=E4=BB=B6=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=98=BE=E7=A4=BA=EF=BC=8C=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/ios/Podfile.lock | 2 +- .../attachment_viewer_controller.dart | 4 +- .../car_info/attachment_viewer_page.dart | 5 +- .../certificate_viewer_controller.dart | 70 ++++++++- .../car_info/certificate_viewer_page.dart | 135 +++++++++++++----- .../pages/c_page/reservation/controller.dart | 32 +++++ .../lib/pages/c_page/reservation/view.dart | 36 +++-- ln_jq_app/pubspec.lock | 4 +- ln_jq_app/pubspec.yaml | 2 +- 9 files changed, 231 insertions(+), 59 deletions(-) diff --git a/ln_jq_app/ios/Podfile.lock b/ln_jq_app/ios/Podfile.lock index a681750..799a4c7 100644 --- a/ln_jq_app/ios/Podfile.lock +++ b/ln_jq_app/ios/Podfile.lock @@ -95,7 +95,7 @@ SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf - flutter_pdfview: 54e283d5851b0b247b3cc57877d35f1a05a204de + flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb diff --git a/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_controller.dart b/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_controller.dart index 105494f..a9a4f45 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_controller.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_controller.dart @@ -48,9 +48,9 @@ class AttachmentViewerController extends GetxController { } }, ); - + localFilePath.value = savePath; - + } catch (e) { showErrorToast('PDF文件加载失败,请检查网络或文件链接'); print('PDF Download Error: $e'); diff --git a/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_page.dart b/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_page.dart index aaf07b9..666288c 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_page.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_page.dart @@ -40,11 +40,10 @@ class AttachmentViewerPage extends GetView { if (controller.fileType == 'pdf') { if (controller.localFilePath.isNotEmpty) { return PDFView( - key: ValueKey(controller.localFilePath.value), filePath: controller.localFilePath.value, enableSwipe: true, - swipeHorizontal: false, - autoSpacing: false, + swipeHorizontal: true, + autoSpacing: true, pageFling: true, onRender: (pages) { print("PDF rendered with $pages pages."); diff --git a/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_controller.dart b/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_controller.dart index 1ce93eb..b431d90 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_controller.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_controller.dart @@ -1,21 +1,77 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:path_provider/path_provider.dart'; import 'attachment_viewer_page.dart'; -class CertificateViewerController extends GetxController { +class CertificateViewerController extends GetxController with BaseControllerMixin{ late final String title; late final List attachments; + // --- 新增: 状态管理 --- + /// 用于存储网络PDF的本地路径,key是网络url,value是本地路径 + final RxMap localPdfPaths = {}.obs; + + /// 用于跟踪每个附件的加载状态,key是网络url + final RxMap isLoading = {}.obs; + + @override + String get builderId => "certificateviewer"; + @override void onInit() { super.onInit(); - // 从 Get.to 的 arguments 中获取标题和附件列表 title = Get.arguments['title'] ?? '证件详情'; attachments = List.from(Get.arguments['attachments'] ?? []); + + // --- 新增: 初始化时开始加载所有PDF --- + _loadAllPdfs(); } - /// 导航到通用的附件查看器页面 + /// 遍历所有附件,如果是PDF则进行下载 + void _loadAllPdfs() { + for (var url in attachments) { + if (isPdf(url)) { + _downloadPdf(url); + } + } + } + + /// 下载单个PDF文件 + Future _downloadPdf(String url) async { + if (url.isEmpty) return; + + // 开始加载 + isLoading[url] = true; + + try { + final dio = Dio(); + final Directory tempDir = await getTemporaryDirectory(); + final String savePath = '${tempDir.path}/${url.split('/').last}'; + + // 检查文件是否已存在,避免重复下载 + if (await File(savePath).exists()) { + localPdfPaths[url] = savePath; + isLoading[url] = false; + return; + } + + await dio.download(url, savePath); + + // 下载成功后,更新本地路径 + localPdfPaths[url] = savePath; + } catch (e) { + print('PDF download error for $url: $e'); + // 出错时也可以更新状态,以便UI显示错误提示 + } finally { + // 结束加载 + isLoading[url] = false; + } + } + + /// 导航到通用的附件查看器页面 (此方法保持不变) void openAttachment(String url) { if (url.isEmpty) { showErrorToast('附件链接无效'); @@ -23,15 +79,17 @@ class CertificateViewerController extends GetxController { } Get.to( - () => const AttachmentViewerPage(), + () => const AttachmentViewerPage(), arguments: { - 'url': url, + 'url': url, }, ); } - /// 检查 URL 是否为 PDF,以便在视图中显示不同的图标 + /// 检查 URL 是否为 PDF (此方法保持不变) bool isPdf(String url) { return url.toLowerCase().endsWith('.pdf'); } + + } diff --git a/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_page.dart b/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_page.dart index 11a88e5..7bbc23e 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_page.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_page.dart @@ -1,51 +1,114 @@ import 'package:flutter/material.dart'; +import 'package:flutter_pdfview/flutter_pdfview.dart'; // 引入PDFView import 'package:get/get.dart'; +import 'package:getx_scaffold/common/common.dart'; import 'certificate_viewer_controller.dart'; class CertificateViewerPage extends GetView { - const CertificateViewerPage({Key? key}) : super(key: key); + const CertificateViewerPage({super.key}); @override Widget build(BuildContext context) { - Get.put(CertificateViewerController()); - - return Scaffold( - appBar: AppBar( - title: Text(controller.title), - centerTitle: true, - ), - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), - itemCount: controller.attachments.length, - itemBuilder: (context, index) { - final url = controller.attachments[index]; - // 从 URL 中提取文件名用于显示 - final fileName = url.split('/').last; - - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - leading: Icon( - controller.isPdf(url) - ? Icons.picture_as_pdf_rounded // PDF 图标 - : Icons.image_rounded, // 图片图标 - color: controller.isPdf(url) ? Colors.red.shade700 : Colors.blue.shade700, - size: 32, + return GetBuilder( + init: CertificateViewerController(), + id: 'certificateviewer', + builder: (_) { + return Scaffold( + appBar: AppBar(title: Text(controller.title)), + body: Column( + children: [ + SizedBox(height: 16,), + Text( + "点击可查看大图", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - title: Text( - fileName, - style: const TextStyle(fontSize: 14), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: controller.attachments.length, + itemBuilder: (context, index) { + final url = controller.attachments[index]; + return _buildAttachmentItem(url); + }, + ), ), - trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: Colors.grey), - onTap: () => controller.openAttachment(url), - ), - ); - }, + ], + ), + ); + }, + ); + } + + /// 构建单个附件的显示项 + Widget _buildAttachmentItem(String url) { + return GestureDetector( + onTap: () { + controller.openAttachment(url); + }, // 点击跳转到详情页 + child: Card( + margin: const EdgeInsets.only(bottom: 16.0), + clipBehavior: Clip.antiAlias, // 确保内容不会溢出Card的圆角 + elevation: 4, + // 等比缩放展示 + child: AspectRatio( + aspectRatio: 4 / 3, + child: controller.isPdf(url) + ? Obx(() { + final bool loading = controller.isLoading[url] ?? true; + final String? localPath = controller.localPdfPaths[url]; + + if (loading) { + return _buildLoadingIndicator(); + } else if (localPath != null && localPath.isNotEmpty) { + return IgnorePointer( + ignoring: true, // 设置为 true 来忽略所有指针事件 + child: PDFView( + fitEachPage: true, + filePath: localPath, + fitPolicy: FitPolicy.WIDTH, + // 适配宽度 + enableSwipe: false, + swipeHorizontal: false, + autoSpacing: false, + pageFling: false, + preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转 + ), + ); + } else { + // PDF加载失败 + return _buildErrorIndicator(); + } + }) + : Image.network( + url, + fit: BoxFit.contain, + // 图片加载时显示loading + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return _buildLoadingIndicator(); + }, + // 图片加载失败时显示错误 + errorBuilder: (context, error, stackTrace) { + return _buildErrorIndicator(); + }, + ), + ), ), ); } + + // 辅助Widget:加载中指示器 + Widget _buildLoadingIndicator() { + return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + } + + // 辅助Widget:错误指示器 + Widget _buildErrorIndicator() { + return const SizedBox( + height: 200, + child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)), + ); + } } 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 fc95862..0361370 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -809,6 +809,25 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } void getSiteList() async { + if(StorageService.to.phone == "13344444444"){ + //该账号给stationOptions手动添加一个数据 + final testStation = StationModel( + hydrogenId: '1142167389150920704', + name: '羚牛氢能演示加氢站', + address: '上海市嘉定区于田南路111号于田大厦', + price: '35.00', // 价格 + siteStatusName: '营运中', // 状态 + isSelect: 1, // 默认可选 + ); + // 使用 assignAll 可以确保列表只包含这个测试数据 + stationOptions.assignAll([testStation]); + + if (stationOptions.isNotEmpty) { + selectedStationId.value = stationOptions.first.hydrogenId; + } + return; + } + showLoading("加载中"); final originalHeaders = Map.from(HttpService.to.dio.options.headers); try { @@ -849,6 +868,19 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } else { showToast('站点列表已刷新'); } + + // 找到第一个可选的站点作为默认值 + if (stationOptions.isNotEmpty) { + final firstSelectable = stationOptions.firstWhere( + (station) => station.isSelect == 1, + orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个 + ); + selectedStationId.value = firstSelectable.hydrogenId; + } else { + // 如果列表为空,确保 selectedStationId 也为空 + selectedStationId.value = null; + } + } catch (e) { showToast('数据异常'); } 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 787617a..f364712 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/view.dart @@ -437,20 +437,40 @@ class ReservationPage extends GetView { ), ) .toList(), - value: controller.selectedStationId.value, + value: + // 当前的站点 处理默认 + controller.selectedStationId.value ?? + (controller.stationOptions.isNotEmpty + ? controller.stationOptions.first.hydrogenId + : null), // 当前选中的是站点ID onChanged: (value) { if (value != null) { controller.selectedStationId.value = value; } }, - customButton: controller.selectedStationId.value == null - ? null // 未选择时,显示默认的 hint - : _buildSelectedStationButton( - controller.stationOptions.firstWhere( - (s) => s.hydrogenId == controller.selectedStationId.value, - ), - ), + customButton: Obx(() { + // 优先从已选中的 ID 查找 + var selectedStation = controller.stationOptions.firstWhereOrNull( + (s) => s.hydrogenId == controller.selectedStationId.value, + ); + + // 如果找不到已选中的(比如 ID 为空或列表里没有),并且列表不为空,则取第一个作为默认 + final stationToShow = + selectedStation ?? + (controller.stationOptions.isNotEmpty + ? controller.stationOptions.first + : null); + + // 如果有要显示的站点,就构建按钮 + if (stationToShow != null) { + return _buildSelectedStationButton(stationToShow); + } + + // 否则,返回一个空占位符,让 hint 生效 + // DropdownButton2 内部会判断,如果 customButton 返回的不是一个有效Widget(或根据其内部逻辑),就会显示 hint + return const SizedBox.shrink(); + }), buttonStyleData: ButtonStyleData( height: 40, // 增加高度以容纳两行文字 padding: const EdgeInsets.symmetric(horizontal: 12.0), diff --git a/ln_jq_app/pubspec.lock b/ln_jq_app/pubspec.lock index 18e7f11..018266c 100644 --- a/ln_jq_app/pubspec.lock +++ b/ln_jq_app/pubspec.lock @@ -399,10 +399,10 @@ packages: dependency: "direct main" description: name: flutter_pdfview - sha256: a9055bf920c7095bf08c2781db431ba23577aa5da5a056a7152dc89a18fbec6f + sha256: c0b2cc4ebf461a5a4bb9312a165222475a7d93845c7a0703f4abb7f442eb6d54 url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.2" + version: "1.4.3" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/ln_jq_app/pubspec.yaml b/ln_jq_app/pubspec.yaml index 5ae1ee7..93d5242 100644 --- a/ln_jq_app/pubspec.yaml +++ b/ln_jq_app/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: image_picker: ^1.2.1 # 用于从相册选择图片 image: ^4.5.4 zxing_lib: ^1.1.4 - flutter_pdfview: 1.3.2 #显示pdf + flutter_pdfview: 1.4.3 #显示pdf photo_view: ^0.15.0 #操作图片 flutter_inappwebview: ^6.1.5 # WebView插件 geolocator: ^14.0.2 # 获取精确定位 From 98cac8a0a5342b032c07f03daa0d391f96dabe11 Mon Sep 17 00:00:00 2001 From: userGyl Date: Tue, 16 Dec 2025 16:18:28 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E9=A2=84=E7=BA=A6=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=AD=9B=E9=80=89=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/c_page/reservation/controller.dart | 242 +++-------------- .../reservation_list_bottomsheet.dart | 245 ++++++++++++++++++ .../lib/pages/c_page/reservation/view.dart | 28 +- 3 files changed, 306 insertions(+), 209 deletions(-) create mode 100644 ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart 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 0361370..62f66b5 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -434,7 +434,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { //打开预约列表 Future.delayed(const Duration(milliseconds: 500), () { - getReservationList(); + getReservationList(showPopup: true, addStatus: ''); }); } else { showToast(result.error); @@ -446,16 +446,16 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } /// 状态变量:是否有预约数据 - bool hasReservationData = false; - + final RxBool hasReservationData = false.obs; // 新增预约数据列表 - List reservationList = []; - + final RxList reservationList = [].obs; + final RxBool shouldShowReservationList = false.obs; // --- 用于防抖的 Timer --- Timer? _debounce; //查看预约列表 - void getReservationList() async { + void getReservationList({bool showPopup = false, String? addStatus}) async { + // 增加 addStatus 参数 if (_debounce?.isActive ?? false) { return; } @@ -464,19 +464,26 @@ class C_ReservationController extends GetxController with BaseControllerMixin { showLoading("加载中"); try { + + final Map requestData = { + 'phone': StorageService.to.phone, + 'pageNum': 1, + 'pageSize': 50, + }; + // 将 addStatus 参数添加到请求中 + if (addStatus != null && addStatus.isNotEmpty) { + requestData['addStatus'] = addStatus; + } + var response = await HttpService.to.post( "appointment/orderAddHyd/driverOrderPage", - data: { - 'phone': StorageService.to.phone, // 使用从 renderData 中获取到的 name - 'pageNum': 1, - 'pageSize': 50, // 暂时不考虑分页,一次获取30条 - }, + data: requestData, ); if (response == null || response.data == null) { showToast('暂时无法获取预约数据'); - hasReservationData = false; - reservationList = []; + hasReservationData.value = false; + reservationList.clear(); return; } @@ -485,13 +492,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin { if (baseModel.code == 0 && baseModel.data != null) { final dataMap = baseModel.data as Map; final List listFromServer = dataMap['records'] ?? []; - reservationList = listFromServer.map((item) { + + // 使用 .value 来更新响应式列表 + reservationList.value = listFromServer.map((item) { return ReservationModel.fromJson(item as Map); }).toList(); - // 根据列表是否为空来更新 hasReservationData 状态 - hasReservationData = reservationList.isNotEmpty; - + // 更新 hasEdit 状态 for (var reservation in reservationList) { try { // 获取当前时间和预约的结束时间 @@ -510,206 +517,27 @@ class C_ReservationController extends GetxController with BaseControllerMixin { reservation.hasEdit = false; } } + + hasReservationData.value = reservationList.isNotEmpty; + + if (showPopup) { + shouldShowReservationList.value = true; + } + } else { showToast(baseModel.message); - hasReservationData = false; - reservationList = []; // 清空列表 + hasReservationData.value = false; + reservationList.clear(); } } catch (e) { showToast('获取预约数据失败'); - hasReservationData = false; - reservationList = []; // 清空列表 + hasReservationData.value = false; + reservationList.clear(); } finally { dismissLoading(); } - - Get.bottomSheet( - Container( - height: Get.height * 0.55, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Column( - children: [ - //标题 - Container( - padding: const EdgeInsets.fromLTRB(20, 15, 20, 15), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '我的预约', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - ElevatedButton( - onPressed: () => Get.back(), - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: Colors.grey[200], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - ), - child: const Text('关闭', style: TextStyle(color: Colors.black54)), - ), - ], - ), - ), - const Divider(height: 1), - Expanded( - child: !hasReservationData - ? Container( - margin: EdgeInsets.only(top: 40), - child: TextX.bodyLarge('暂无预约', weight: FontWeight.w500), - ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: reservationList.length, - itemBuilder: (context, index) { - final ReservationModel reservation = reservationList[index]; - return Card( - color: Colors.white, - margin: const EdgeInsets.only(bottom: 12.0), - elevation: 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - decoration: BoxDecoration( - color: const Color( - 0xFFE6F7FF, - ), // Light blue background - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: const Color(0xFF91D5FF), - ), // Blue border - ), - child: Text( - reservation.stateName + - "-" + - reservation.addStatusName, - style: const TextStyle( - color: Color(0xFF1890FF), - fontWeight: FontWeight.bold, - ), - ), - ), - !reservation.hasEdit - ? SizedBox() - : GestureDetector( - onTap: () async { - var result = await Get.to( - () => ReservationEditPage(), - arguments: { - 'reservation': reservation, - 'difference': difference, - }, - binding: BindingsBuilder(() { - Get.put(ReservationEditController()); - }), - preventDuplicates: false, - ); - if (result == true) { - Get.back(); - getReservationList(); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: const Color( - 0xFFFFF7E6, - ), // Light orange background - borderRadius: BorderRadius.circular(12), - ), - child: Text( - "修改", - style: const TextStyle( - color: Color(0xFFFA8C16), - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 12), - _buildDetailRow('车牌号:', reservation.plateNumber), - _buildDetailRow('预约日期:', reservation.date), - _buildDetailRow('预约氢量:', reservation.hydAmount), - _buildDetailRow('加氢站:', reservation.stationName), - _buildDetailRow('开始时间:', reservation.startTime), - _buildDetailRow('结束时间:', reservation.endTime), - _buildDetailRow('联系人:', reservation.contacts), - _buildDetailRow('联系电话:', reservation.phone), - reservation.addStatus == "5" - ? _buildDetailRow('拒绝原因:', reservation.rejectReason) - : SizedBox(), - ], - ), - ), - ); - }, - ), - ), - ], - ), - ), - isScrollControlled: true, - backgroundColor: - Colors.transparent, // Make background transparent to see the rounded corners - ); } - Widget _buildDetailRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 85, - child: Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), - ), - Expanded( - child: Text( - value, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: Colors.black87, - ), - ), - ), - ], - ), - ); - } String workEfficiency = "0"; String fillingWeight = "0"; diff --git a/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart b/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart new file mode 100644 index 0000000..2cad37b --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:getx_scaffold/common/index.dart'; +import 'package:ln_jq_app/pages/c_page/reservation/controller.dart'; +import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart'; +import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart'; + +//加氢预约列表 +class ReservationListBottomSheet extends StatefulWidget { + const ReservationListBottomSheet({super.key}); + + @override + State createState() => _ReservationListBottomSheetState(); +} + +class _ReservationListBottomSheetState extends State { + final C_ReservationController _controller = Get.find(); + + final Map _statusOptions = { + '': '所有状态', // 增加一个“所有状态”的选项 + '0': '待加氢', + '1': '加氢完成', + '2': '未加氢', + '5': '拒绝加氢', + }; + String _selectedStatus = ''; // 默认选中 '0' (待加氢) + + @override + void initState() { + super.initState(); + // Widget 初始化时,立即调用接口加载默认状态(待加氢)的数据 + _controller.getReservationList(addStatus: _selectedStatus); + } + + @override + Widget build(BuildContext context) { + return Container( + height: Get.height * 0.55, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Column( + children: [ + // 构建标题和下拉框 + _buildHeader(), + const Divider(height: 1), + // 下拉筛选框 + _buildChoice(), + // 构建列表(使用 Obx 监听数据变化) + _buildList(), + ], + ), + ); + } + + Container _buildChoice() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 8, 0, 0), + alignment: AlignmentGeometry.centerLeft, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton( + value: _selectedStatus, + underline: const SizedBox.shrink(), // 隐藏下划线 + items: _statusOptions.entries.map((entry) { + return DropdownMenuItem( + value: entry.key, + child: Text(entry.value), + ); + }).toList(), + onChanged: (newValue) { + if (newValue != null) { + setState(() { + _selectedStatus = newValue; + }); + // 当选择新状态时,调用接口刷新数据 + _controller.getReservationList(addStatus: _selectedStatus); + } + }, + ), + ), + ); + } + + /// 构建标题、关闭按钮和下拉筛选框 + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 8, 8, 8), + child: Stack( + alignment: Alignment.center, + children: [ + Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.grey[200], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), + child: const Text('关闭', style: TextStyle(color: Colors.black54)), + ), + ), + ], + ), + ); + } + + /// 构建预约列表 + Widget _buildList() { + return Expanded( + child: Obx(() { + if (!_controller.hasReservationData.value) { + return Container( + margin: const EdgeInsets.only(top: 40), + child: TextX.bodyLarge('暂无该状态下的预约', weight: FontWeight.w500), + ); + } + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _controller.reservationList.length, + itemBuilder: (context, index) { + final reservation = _controller.reservationList[index]; + return Card( + color: Colors.white, + margin: const EdgeInsets.only(bottom: 12.0), + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 状态标签 + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: const Color(0xFFE6F7FF), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF91D5FF)), + ), + child: Text( + "${reservation.stateName}-${reservation.addStatusName}", + style: const TextStyle( + color: Color(0xFF1890FF), + fontWeight: FontWeight.bold, + ), + ), + ), + // 修改按钮 (仅在 hasEdit 为 true 时显示) + if (reservation.hasEdit) + GestureDetector( + onTap: () async { + var result = await Get.to( + () => ReservationEditPage(), + arguments: { + 'reservation': reservation, + 'difference': _controller.difference, + }, + binding: BindingsBuilder(() { + Get.put(ReservationEditController()); + }), + preventDuplicates: false, + ); + if (result == true) { + _controller.getReservationList( + addStatus: _selectedStatus, + ); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFF7E6), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + "修改", + style: TextStyle( + color: Color(0xFFFA8C16), + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + _buildDetailRow('车牌号:', reservation.plateNumber), + _buildDetailRow('预约日期:', reservation.date), + _buildDetailRow('预约氢量:', reservation.hydAmount), + _buildDetailRow('加氢站:', reservation.stationName), + _buildDetailRow('开始时间:', reservation.startTime), + _buildDetailRow('结束时间:', reservation.endTime), + _buildDetailRow('联系人:', reservation.contacts), + _buildDetailRow('联系电话:', reservation.phone), + reservation.addStatus == "5" + ? _buildDetailRow('拒绝原因:', reservation.rejectReason) + : SizedBox(), + ], + ), + ), + ); + }, + ); + }), + ); + } + + /// 构建详情行 (这是一个辅助Widget) + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + children: [ + Text(label, style: const TextStyle(color: Colors.grey)), + const SizedBox(width: 8), + Expanded( + child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), + ), + ], + ), + ); + } +} 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 f364712..0d95a51 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/view.dart @@ -7,10 +7,13 @@ import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/storage_service.dart'; import 'controller.dart'; +import 'reservation_list_bottomsheet.dart'; ///加氢预约 class ReservationPage extends GetView { - const ReservationPage({super.key}); + ReservationPage({super.key}); + + bool init = false; @override Widget build(BuildContext context) { @@ -18,6 +21,10 @@ class ReservationPage extends GetView { init: C_ReservationController(), id: 'reservation', builder: (_) { + if (!init) { + _setupListener(context); + init = true; + } return Scaffold( backgroundColor: Colors.grey[100], body: GestureDetector( @@ -299,7 +306,9 @@ class ReservationPage extends GetView { const SizedBox(width: 16), Expanded( child: OutlinedButton( - onPressed: controller.getReservationList, + onPressed: () { + controller.getReservationList(showPopup: true, addStatus: ''); + }, style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 38), // 高度与另一个按钮保持一致 side: const BorderSide(color: Colors.blue), @@ -326,6 +335,21 @@ class ReservationPage extends GetView { ); } + void _setupListener(BuildContext context) { + ever(controller.shouldShowReservationList, (bool shouldShow) { + if (shouldShow) { + Get.bottomSheet( + const ReservationListBottomSheet(), + isScrollControlled: true, // 允许弹窗使用更多屏幕高度 + backgroundColor: Colors.transparent, + ); + + // 重要:显示后立即将信号重置为 false,防止不必要的重复弹出 + controller.shouldShowReservationList.value = false; + } + }); + } + // 表单中的可点击行 (用于日期和时间选择) Widget _buildPickerRow({ required String label, From fe2ce75cecb99d87ddb0d073c78b700fe9bf39f8 Mon Sep 17 00:00:00 2001 From: userGyl Date: Wed, 17 Dec 2025 09:18:34 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E8=BD=A6=E8=BE=86=E6=8E=A5=E5=8F=A3=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=90=8E=E6=9F=A5=E8=AF=A2=E8=BD=A6=E8=BE=86=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/pages/c_page/car_info/controller.dart | 18 ++--- .../pages/c_page/reservation/controller.dart | 20 +++--- ln_jq_app/lib/pages/login/view.dart | 18 +++++ ln_jq_app/lib/pages/qr_code/controller.dart | 72 ++++++++++++++----- 4 files changed, 95 insertions(+), 33 deletions(-) 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 ac95c15..b88a5f1 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 @@ -28,11 +28,14 @@ class CarInfoController extends GetxController with BaseControllerMixin { super.onInit(); getUserBindCarInfo(); } + @override void onReady() { super.onReady(); // 如果未绑定车辆,且本次会话尚未提示过,则弹出提示 - if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) { + if (!StorageService.to.hasShownBindVehicleDialog && + StorageService.to.isLoggedIn && + !StorageService.to.hasVehicleInfo) { Future.delayed(const Duration(milliseconds: 500), () { DialogX.to.showConfirmDialog( title: '当前尚未绑定车辆', @@ -71,7 +74,7 @@ class CarInfoController extends GetxController with BaseControllerMixin { final response = await HttpService.to.get( 'appointment/vehicle/getPicInfoByVin?vin=$vin', ); - + if (response != null && response.data != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0 && result.data != null) { @@ -88,7 +91,9 @@ class CarInfoController extends GetxController with BaseControllerMixin { // 将解析出的 URL 列表赋值给对应的 RxList drivingAttachments.assignAll(parseAttachments(data['drivingAttachment'])); operationAttachments.assignAll(parseAttachments(data['operationAttachment'])); - hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment'])); + hydrogenationAttachments.assignAll( + parseAttachments(data['hydrogenationAttachment']), + ); registerAttachments.assignAll(parseAttachments(data['registerAttachment'])); } } @@ -98,7 +103,7 @@ class CarInfoController extends GetxController with BaseControllerMixin { /// 跳转到证件查看页面 void navigateToCertificateViewer(String title, List attachments) { - if(!StorageService.to.hasVehicleInfo){ + if (!StorageService.to.hasVehicleInfo) { showToast('请先绑定车辆'); return; } @@ -108,10 +113,7 @@ class CarInfoController extends GetxController with BaseControllerMixin { } Get.to( () => const CertificateViewerPage(), - arguments: { - 'title': title, - 'attachments': attachments, - }, + arguments: {'title': title, 'attachments': attachments}, ); } } 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 62f66b5..d558341 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -447,9 +447,11 @@ class C_ReservationController extends GetxController with BaseControllerMixin { /// 状态变量:是否有预约数据 final RxBool hasReservationData = false.obs; + // 新增预约数据列表 final RxList reservationList = [].obs; final RxBool shouldShowReservationList = false.obs; + // --- 用于防抖的 Timer --- Timer? _debounce; @@ -464,7 +466,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin { showLoading("加载中"); try { - final Map requestData = { 'phone': StorageService.to.phone, 'pageNum': 1, @@ -523,7 +524,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin { if (showPopup) { shouldShowReservationList.value = true; } - } else { showToast(baseModel.message); hasReservationData.value = false; @@ -538,7 +538,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } } - String workEfficiency = "0"; String fillingWeight = "0"; String fillingTimes = "0"; @@ -637,14 +636,16 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } void getSiteList() async { - if(StorageService.to.phone == "13344444444"){ + if (StorageService.to.phone == "13344444444") { //该账号给stationOptions手动添加一个数据 final testStation = StationModel( hydrogenId: '1142167389150920704', name: '羚牛氢能演示加氢站', address: '上海市嘉定区于田南路111号于田大厦', - price: '35.00', // 价格 - siteStatusName: '营运中', // 状态 + price: '35.00', + // 价格 + siteStatusName: '营运中', + // 状态 isSelect: 1, // 默认可选 ); // 使用 assignAll 可以确保列表只包含这个测试数据 @@ -700,7 +701,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { // 找到第一个可选的站点作为默认值 if (stationOptions.isNotEmpty) { final firstSelectable = stationOptions.firstWhere( - (station) => station.isSelect == 1, + (station) => station.isSelect == 1, orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个 ); selectedStationId.value = firstSelectable.hydrogenId; @@ -708,7 +709,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin { // 如果列表为空,确保 selectedStationId 也为空 selectedStationId.value = null; } - } catch (e) { showToast('数据异常'); } @@ -719,7 +719,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin { HttpService.to.dio.options.headers = originalHeaders; // 如果未绑定车辆,且本次会话尚未提示过,则弹出提示 - if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) { + if (!StorageService.to.hasShownBindVehicleDialog && + StorageService.to.isLoggedIn && + !StorageService.to.hasVehicleInfo) { Future.delayed(const Duration(milliseconds: 500), () { DialogX.to.showConfirmDialog( title: '当前尚未绑定车辆', diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index d47b141..2201a04 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:ln_jq_app/common/login_util.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/styles/theme.dart'; import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; @@ -174,6 +175,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix showToast('登录失败:无法获取凭证'); return; } + //登录信息处理 try { var result = BaseModel.fromJson(responseData.data); String token = result.data['token'] ?? ''; @@ -188,6 +190,22 @@ class _LoginPageState extends State with SingleTickerProviderStateMix name: name, phone: phone, ); + + //登录后查询已绑定车辆信息 + var carInfo = await HttpService.to.get( + "appointment/driver/getTruckInfoByDriver?phone=$phone" + ); + if (carInfo != null) { + var carInforesult = BaseModel.fromJson(carInfo.data); + if (carInforesult.data != null) { + final vehicle = VehicleInfo.fromJson(carInforesult.data as Map); + //保存使用 + await StorageService.to.saveVehicleInfo(vehicle); + } + } + + + //页面操作 dismissLoading(); showToast('登录成功,欢迎您'); Get.offAll(() => BaseWidgetsPage()); diff --git a/ln_jq_app/lib/pages/qr_code/controller.dart b/ln_jq_app/lib/pages/qr_code/controller.dart index 40f0e6d..6367afb 100644 --- a/ln_jq_app/lib/pages/qr_code/controller.dart +++ b/ln_jq_app/lib/pages/qr_code/controller.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:image/image.dart' as img; import 'package:image_picker/image_picker.dart'; @@ -125,14 +124,12 @@ class QrCodeController extends GetxController } void requestPermission() async { - if(Platform.isIOS){ + if (Platform.isIOS) { var status = await Permission.camera.request(); if (status.isGranted) { - } - else if (status.isPermanentlyDenied) { + } else if (status.isPermanentlyDenied) { openAppSettings(); - } - else { + } else { showErrorToast('需要相机权限才能扫描二维码'); } return; @@ -153,36 +150,51 @@ class QrCodeController extends GetxController scanFromGallery(); } } - if(Platform.isIOS){ + if (Platform.isIOS) { var status = await Permission.photos.request(); print("权限状态: $status"); // 在控制台看这个输出 if (status.isGranted) { scanFromGallery(); - } - else if (status.isPermanentlyDenied) { + } else if (status.isPermanentlyDenied) { openAppSettings(); - } - else { + } else { showErrorToast('需要相册权限才能从相册中选择图片'); } } } - //扫码结果处理 - void renderResult(String resultStr) async { + //扫码结果处理 //如果绑定接口返回的data为null 需要手动编辑车牌 + void renderResult(String resultStr, {plateNumber}) async { showLoading("正在获取车辆信息..."); try { - var responseData = await HttpService.to.get( + /*var responseData = await HttpService.to.get( "appointment/truck/base-info?vin=$resultStr", + );*/ + + 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 || responseData.data == null) { + if (responseData == null) { showToast('无法获取车辆信息,请检查网络或稍后重试'); resumeScanner(); return; } - var result = BaseModel.fromJson(responseData.data); + + if (result.data == null) { + showBindDialog(resultStr); + return; + } + final vehicle = VehicleInfo.fromJson(result.data as Map); //保存使用 await StorageService.to.saveVehicleInfo(vehicle); @@ -199,6 +211,34 @@ class QrCodeController extends GetxController } } + void showBindDialog(String resultStr) { + final TextEditingController plateNumberController = TextEditingController(); + DialogX.to.showNoticeDialog( + icon: DialogIcon.info, + title: '请输入车牌号', + barrierDismissible: false, + content: TextField( + controller: plateNumberController, // 绑定 controller + autofocus: false, // 弹窗出现时自动获取焦点,方便用户直接输入 + decoration: const InputDecoration( + hintText: '请输入完整的车牌号', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12.0), + ), + ), + confirmText: '确认', + onConfirm: () { + final String plateNumber = plateNumberController.text.trim(); + if (plateNumber.isEmpty) { + resumeScanner(); + showToast("请输入车牌号"); + return; + } + renderResult(resultStr, plateNumber: plateNumber); + }, + ); + } + @override void onClose() { qrViewController?.dispose(); From 42355bd1eff96d50be933bc3062e0e9551641aab Mon Sep 17 00:00:00 2001 From: userGyl Date: Wed, 17 Dec 2025 17:40:02 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E7=AB=99=E7=82=B9-=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/assets/html/car.png | Bin 0 -> 6754 bytes ln_jq_app/assets/html/map.html | 4 +- .../lib/pages/b_page/history/controller.dart | 186 +++++++++++++ ln_jq_app/lib/pages/b_page/history/view.dart | 249 ++++++++++++++++++ ln_jq_app/lib/pages/b_page/site/view.dart | 66 +++-- 5 files changed, 476 insertions(+), 29 deletions(-) create mode 100644 ln_jq_app/assets/html/car.png create mode 100644 ln_jq_app/lib/pages/b_page/history/controller.dart create mode 100644 ln_jq_app/lib/pages/b_page/history/view.dart diff --git a/ln_jq_app/assets/html/car.png b/ln_jq_app/assets/html/car.png new file mode 100644 index 0000000000000000000000000000000000000000..5f5ba0a947ed485e890db49386838b4bad502d0c GIT binary patch literal 6754 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 UCptFqe;w2Hv~OuuYS>5o7klGh?EnA( literal 0 HcmV?d00001 diff --git a/ln_jq_app/assets/html/map.html b/ln_jq_app/assets/html/map.html index 3ef4926..3df0453 100644 --- a/ln_jq_app/assets/html/map.html +++ b/ln_jq_app/assets/html/map.html @@ -248,8 +248,8 @@ marker = new AMap.Marker({ map: map, position: position, - icon: "https://webapi.amap.com/images/car.png", - offset: new AMap.Pixel(-26, -13), + icon: "car.png", + offset: new AMap.Pixel(-23.5, -15), autoRotation: true, angle: isNaN(rawAngle) ? 0 : rawAngle, }); diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart new file mode 100644 index 0000000..3a17016 --- /dev/null +++ b/ln_jq_app/lib/pages/b_page/history/controller.dart @@ -0,0 +1,186 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:ln_jq_app/common/model/base_model.dart'; +import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel + +class HistoryController extends GetxController { + // --- 定义 API 需要的日期格式化器 --- + final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd'); + + // 默认查询最近7天 + final Rx startDate = DateTime.now().subtract(const Duration(days: 7)).obs; + final Rx endDate = DateTime.now().obs; + final TextEditingController plateNumberController = TextEditingController(); + + final RxString totalHydrogen = '0 kg'.obs; + final RxString totalCompletions = '0 次'.obs; + + final RxList historyList = [].obs; + final RxBool isLoading = true.obs; + final RxBool hasData = false.obs; + + String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value); + + String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value); + + @override + void onInit() { + super.onInit(); + fetchHistoryData(); + } + + Future fetchHistoryData() async { + isLoading.value = true; + try { + var response = await HttpService.to.post( + "appointment/orderAddHyd/sitOrderPage", + data: { + // --- 直接使用 DateFormat 来格式化日期 --- + 'startDate': _apiDateFormat.format(startDate.value), + 'endDate': _apiDateFormat.format(endDate.value), + 'plateNumber': plateNumberController.text, + 'pageNum': 1, + 'pageSize': 50, + 'stationName': "", // 加氢站名称 + }, + ); + + if (response == null || response.data == null) { + showToast('无法获取历史记录'); + _resetData(); + return; + } + + final baseModel = BaseModel.fromJson(response.data); + if (baseModel.code == 0 && baseModel.data != null) { + final dataMap = baseModel.data as Map; + + totalHydrogen.value = '${dataMap['totalHydrogen'] ?? 0} kg'; + totalCompletions.value = '${dataMap['totalCompletions'] ?? 0} 次'; + + final List listFromServer = dataMap['records'] ?? []; + historyList.assignAll( + listFromServer + .map((item) => ReservationModel.fromJson(item as Map)) + .toList(), + ); + hasData.value = historyList.isNotEmpty; + } else { + showToast(baseModel.message); + _resetData(); + } + } catch (e) { + showToast('获取历史记录失败: $e'); + _resetData(); + } finally { + isLoading.value = false; + } + } + + void _resetData() { + totalHydrogen.value = '0 kg'; + totalCompletions.value = '0 次'; + historyList.clear(); + hasData.value = false; + } + + void pickDate(BuildContext context, bool isStartDate) { + // 确定当前操作的日期和临时存储变量 + final DateTime initialDate = isStartDate ? startDate.value : endDate.value; + DateTime tempDate = initialDate; + + // 定义全局的最早可选日期 + final DateTime globalMinimumDate = DateTime(2025, 12, 1); + + // 动态计算当前选择器的最小/最大日期范围 + DateTime minimumDate; + DateTime? maximumDate; // 声明为可空,因为两个日期都可能没有最大限制 + + if (isStartDate) { + // 当选择【开始日期】时 它的最小日期就是全局最小日期 + minimumDate = globalMinimumDate; + // 最大日期没有限制 + maximumDate = null; + } else { + // 当选择【结束日期】时 它的最小日期不能早于当前的开始日期 + minimumDate = startDate.value; + // 确认结束日期没有最大限制 --- + //最大日期没有限制 + maximumDate = null; + } + + Get.bottomSheet( + Container( + height: 300, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Column( + children: [ + // 顶部的取消和确认按钮 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('取消', style: TextStyle(color: Colors.grey)), + ), + TextButton( + onPressed: () { + // 4. 确认后,更新对应的日期变量 + if (isStartDate) { + startDate.value = tempDate; + // 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天 + if (tempDate.isAfter(endDate.value)) { + endDate.value = tempDate; + } + } else { + endDate.value = tempDate; + } + Get.back(); + + // 选择日期后自动刷新数据 + fetchHistoryData(); + }, + child: const Text( + '确认', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + const Divider(height: 1), + // 日期选择器 + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: initialDate, + // 应用动态计算好的最小/最大日期 + minimumDate: minimumDate, + maximumDate: maximumDate, + onDateTimeChanged: (DateTime newDate) { + tempDate = newDate; + }, + ), + ), + ], + ), + ), + backgroundColor: Colors.transparent, // 使底部工作表外的区域透明 + ); + } + + @override + void onClose() { + plateNumberController.dispose(); + super.onClose(); + } +} diff --git a/ln_jq_app/lib/pages/b_page/history/view.dart b/ln_jq_app/lib/pages/b_page/history/view.dart new file mode 100644 index 0000000..fb3d9c4 --- /dev/null +++ b/ln_jq_app/lib/pages/b_page/history/view.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ln_jq_app/common/styles/theme.dart'; +import 'package:ln_jq_app/pages/b_page/history/controller.dart'; +import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel + +class HistoryPage extends GetView { + const HistoryPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Get.put(HistoryController()); + + return Scaffold( + appBar: AppBar(title: const Text('历史记录'), centerTitle: true), + body: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + _buildFilterCard(context), + const SizedBox(height: 12), + _buildSummaryCard(), + const SizedBox(height: 12), + _buildListHeader(), + Expanded(child: _buildHistoryList()), + ], + ), + ), + ); + } + + Widget _buildFilterCard(BuildContext context) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)), + const SizedBox(height: 8), + Row( + children: [ + Expanded(child: _buildDateField(context, true)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Text('至'), + ), + Expanded(child: _buildDateField(context, false)), + ], + ), + const SizedBox(height: 16), + const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)), + const SizedBox(height: 8), + SizedBox( + height: 44, + child: TextField( + controller: controller.plateNumberController, + decoration: InputDecoration( + hintText: '请输入车牌号', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + ), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () { + FocusScope.of(context).unfocus(); // Hide keyboard + controller.fetchHistoryData(); + }, + icon: const Icon(Icons.search, size: 20), + label: const Text('查询'), + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 44), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSummaryCard() { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: Obx( + () => Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue), + const SizedBox(width: 1, height: 40, child: VerticalDivider()), + _buildSummaryItem( + '预约完成次数', + controller.totalCompletions.value, + Colors.green, + ), + ], + ), + ), + ), + ); + } + + Widget _buildHistoryList() { + return Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (!controller.hasData.value) { + return const Center(child: Text('没有找到相关记录')); + } + return ListView.builder( + itemCount: controller.historyList.length, + itemBuilder: (context, index) { + final ReservationModel item = controller.historyList[index]; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + title: Text('车牌号: ${item.plateNumber}'), + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '加氢站: ${item.stationName}\n', + style: TextStyle(fontSize: 16), + ), + TextSpan( + text: '时间: ${item.time}\n', + style: TextStyle(fontSize: 16), + ), + TextSpan( + text: '加氢量:', + ), + TextSpan( + text: '${item.amount}', + style: TextStyle(fontSize: 16, color: AppTheme.themeColor), + ), + ], + ), + ) + , + trailing: + // 状态标签 + _buildStatusChip(item.status), + ), + ); + }, + ); + }); + } + + Widget _buildStatusChip(ReservationStatus status) { + String text; + Color color; + switch (status) { + case ReservationStatus.pending: + text = '待加氢'; + color = Colors.orange; + break; + case ReservationStatus.completed: + text = '已加氢'; + color = Colors.greenAccent; + break; + case ReservationStatus.rejected: + text = '拒绝加氢'; + color = Colors.red; + break; + case ReservationStatus.unadded: + text = '未加氢'; + color = Colors.red; + break; + default: + text = '未知状态'; + color = Colors.grey; + break; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.circle, color: color, size: 8), + const SizedBox(width: 4), + Text( + text, + style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } + + Widget _buildDateField(BuildContext context, bool isStart) { + return Obx( + () => InkWell( + onTap: () => controller.pickDate(context, isStart), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(isStart ? controller.formattedStartDate : controller.formattedEndDate), + const Icon(Icons.calendar_today, size: 18, color: Colors.grey), + ], + ), + ), + ), + ); + } + + Widget _buildSummaryItem(String label, String value, Color color) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 8), + Text( + value, + style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold), + ), + ], + ); + } + + Widget _buildListHeader() { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ], + ), + ); + } +} 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 abdc2dc..9012495 100644 --- a/ln_jq_app/lib/pages/b_page/site/view.dart +++ b/ln_jq_app/lib/pages/b_page/site/view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:ln_jq_app/common/styles/theme.dart'; +import 'package:ln_jq_app/pages/b_page/history/view.dart'; import 'controller.dart'; @@ -114,48 +115,59 @@ class SitePage extends GetView { Card( elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), - margin: const EdgeInsets.only(bottom: 12), + margin: EdgeInsets.only(bottom: 12), clipBehavior: Clip.antiAlias, child: Column( children: [ Container( color: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: Row( children: [ - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '今日预约信息', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, + Expanded( + child: GestureDetector( + onTap: () { + controller.renderData(); + }, + child: Row( + children: [ + Text( + '今日预约信息', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), - ), - SizedBox(height: 2), - Text( - 'Reservation Information', - style: TextStyle(fontSize: 12, color: Colors.white70), - ), - ], + SizedBox( + width: 32, + height: 32, + child: const Icon( + Icons.refresh, + size: 18, + color: Colors.white, + ), + ), + ], + ), ), ), - ElevatedButton.icon( + ElevatedButton( onPressed: () { - controller.renderData(); + Get.to(() => const HistoryPage()); }, - icon: const Icon(Icons.refresh, size: 16), - label: const Text('刷新'), style: ElevatedButton.styleFrom( - foregroundColor: Colors.blue, - backgroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12), + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(5), ), + elevation: 2, + ), + child: const Text( + '历史记录', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), ), ], From 95c08818cb63b068d287549edcf58d8c1c16c329 Mon Sep 17 00:00:00 2001 From: userGyl Date: Thu, 18 Dec 2025 09:03:08 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E5=8F=96=E6=B6=88dark=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ln_jq_app/lib/main.dart b/ln_jq_app/lib/main.dart index 5511c90..3bf0db7 100644 --- a/ln_jq_app/lib/main.dart +++ b/ln_jq_app/lib/main.dart @@ -38,7 +38,7 @@ void main() async { // 主题 theme: AppTheme.light, // Dark主题 - darkTheme: AppTheme.dark, + darkTheme: AppTheme.light, // AppTitle title: '小羚羚', // 首页入口 From 3ec56a925ca6f9649f0d63acf8a85f1986ea035c Mon Sep 17 00:00:00 2001 From: userGyl Date: Thu, 18 Dec 2025 10:33:24 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/lib/pages/b_page/history/controller.dart | 10 +++++++--- ln_jq_app/lib/pages/b_page/site/view.dart | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart index 3a17016..335b34a 100644 --- a/ln_jq_app/lib/pages/b_page/history/controller.dart +++ b/ln_jq_app/lib/pages/b_page/history/controller.dart @@ -23,10 +23,14 @@ class HistoryController extends GetxController { String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value); String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value); + String stationName = ""; @override void onInit() { super.onInit(); + + final args = Get.arguments as Map; + stationName = args['stationName'] as String; fetchHistoryData(); } @@ -37,12 +41,12 @@ class HistoryController extends GetxController { "appointment/orderAddHyd/sitOrderPage", data: { // --- 直接使用 DateFormat 来格式化日期 --- - 'startDate': _apiDateFormat.format(startDate.value), - 'endDate': _apiDateFormat.format(endDate.value), + 'startTime': _apiDateFormat.format(startDate.value), + 'endTime': _apiDateFormat.format(endDate.value), 'plateNumber': plateNumberController.text, 'pageNum': 1, 'pageSize': 50, - 'stationName': "", // 加氢站名称 + 'stationName': stationName, // 加氢站名称 }, ); 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 9012495..dd64d9e 100644 --- a/ln_jq_app/lib/pages/b_page/site/view.dart +++ b/ln_jq_app/lib/pages/b_page/site/view.dart @@ -154,7 +154,12 @@ class SitePage extends GetView { ), ElevatedButton( onPressed: () { - Get.to(() => const HistoryPage()); + Get.to( + () => HistoryPage(), + arguments: { + 'stationName': controller.name, + }, + ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade700, From 62ca3888d352e08c57fe21775426e74c516edb45 Mon Sep 17 00:00:00 2001 From: userGyl Date: Thu, 18 Dec 2025 15:20:54 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E5=8E=86=E5=8F=B2=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=8C=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/pages/b_page/history/controller.dart | 29 +++++++++++++++---- ln_jq_app/lib/pages/login/view.dart | 12 ++++++++ ln_jq_app/lib/pages/qr_code/controller.dart | 5 ++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart index 335b34a..e50178b 100644 --- a/ln_jq_app/lib/pages/b_page/history/controller.dart +++ b/ln_jq_app/lib/pages/b_page/history/controller.dart @@ -32,6 +32,30 @@ class HistoryController extends GetxController { final args = Get.arguments as Map; stationName = args['stationName'] as String; fetchHistoryData(); + getAllOrderCounts(); + } + + Future getAllOrderCounts() async { + var response = await HttpService.to.post( + "appointment/orderAddHyd/getAllOrderCounts", + data: { + 'stationName': stationName, // 加氢站名称 + }, + ); + if (response == null || response.data == null) { + totalHydrogen.value = '0 kg'; + totalCompletions.value = '0 次'; + return; + } + try { + final baseModel = BaseModel.fromJson(response.data); + final dataMap = baseModel.data as Map; + totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg'; + totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0} 次'; + } catch (e) { + totalHydrogen.value = '0 kg'; + totalCompletions.value = '0 次'; + } } Future fetchHistoryData() async { @@ -60,9 +84,6 @@ class HistoryController extends GetxController { if (baseModel.code == 0 && baseModel.data != null) { final dataMap = baseModel.data as Map; - totalHydrogen.value = '${dataMap['totalHydrogen'] ?? 0} kg'; - totalCompletions.value = '${dataMap['totalCompletions'] ?? 0} 次'; - final List listFromServer = dataMap['records'] ?? []; historyList.assignAll( listFromServer @@ -83,8 +104,6 @@ class HistoryController extends GetxController { } void _resetData() { - totalHydrogen.value = '0 kg'; - totalCompletions.value = '0 次'; historyList.clear(); hasData.value = false; } diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index 2201a04..73422c9 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -178,6 +178,12 @@ class _LoginPageState extends State with SingleTickerProviderStateMix //登录信息处理 try { var result = BaseModel.fromJson(responseData.data); + + if (result.code != 0) { + showToast(result.error); + return; + } + String token = result.data['token'] ?? ''; String idCard = result.data['idCard'] ?? ''; String name = result.data['name'] ?? ''; @@ -337,6 +343,12 @@ class _LoginPageState extends State with SingleTickerProviderStateMix try { var result = BaseModel.fromJson(responseData.data); + + if (result.code != 0) { + showToast(result.error); + return; + } + String token = result.data['token'] ?? ''; String userId = result.data['userId'] ?? ''; diff --git a/ln_jq_app/lib/pages/qr_code/controller.dart b/ln_jq_app/lib/pages/qr_code/controller.dart index 6367afb..29b8013 100644 --- a/ln_jq_app/lib/pages/qr_code/controller.dart +++ b/ln_jq_app/lib/pages/qr_code/controller.dart @@ -190,6 +190,11 @@ class QrCodeController extends GetxController } var result = BaseModel.fromJson(responseData.data); + if (result.code != 0) { + showToast(result.error); + return; + } + if (result.data == null) { showBindDialog(resultStr); return; From 21a528d6d1f61ce4b54395a4d77fd92fc3f3baf1 Mon Sep 17 00:00:00 2001 From: userGyl Date: Fri, 19 Dec 2025 13:18:04 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=92=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/pages/b_page/history/controller.dart | 9 ++- .../lib/pages/b_page/site/controller.dart | 1 + .../pages/c_page/reservation/controller.dart | 69 +++++++++---------- ln_jq_app/lib/pages/login/view.dart | 2 + 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart index e50178b..63874b8 100644 --- a/ln_jq_app/lib/pages/b_page/history/controller.dart +++ b/ln_jq_app/lib/pages/b_page/history/controller.dart @@ -32,13 +32,16 @@ class HistoryController extends GetxController { final args = Get.arguments as Map; stationName = args['stationName'] as String; fetchHistoryData(); - getAllOrderCounts(); } Future getAllOrderCounts() async { var response = await HttpService.to.post( "appointment/orderAddHyd/getAllOrderCounts", data: { + // --- 直接使用 DateFormat 来格式化日期 --- + 'startTime': _apiDateFormat.format(startDate.value), + 'endTime': _apiDateFormat.format(endDate.value), + 'plateNumber': plateNumberController.text, 'stationName': stationName, // 加氢站名称 }, ); @@ -60,6 +63,10 @@ class HistoryController extends GetxController { Future fetchHistoryData() async { isLoading.value = true; + + //获取数据 + getAllOrderCounts(); + try { var response = await HttpService.to.post( "appointment/orderAddHyd/sitOrderPage", diff --git a/ln_jq_app/lib/pages/b_page/site/controller.dart b/ln_jq_app/lib/pages/b_page/site/controller.dart index a2294f7..29448e5 100644 --- a/ln_jq_app/lib/pages/b_page/site/controller.dart +++ b/ln_jq_app/lib/pages/b_page/site/controller.dart @@ -191,6 +191,7 @@ class SiteController extends GetxController with BaseControllerMixin { showToast('暂时无法获取预约数据'); hasReservationData = false; reservationList = []; + dismissLoading(); return; } 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 d558341..e1ae31a 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -657,7 +657,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { return; } - showLoading("加载中"); + showLoading("加氢站数据加载中"); final originalHeaders = Map.from(HttpService.to.dio.options.headers); try { HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url); @@ -670,49 +670,46 @@ class C_ReservationController extends GetxController with BaseControllerMixin { dismissLoading(); return; } + dismissLoading(); + var result = BaseModel.fromJson(responseData.data); + var stationDataList = result.data['data'] as List; - try { - dismissLoading(); - var result = BaseModel.fromJson(responseData.data); - var stationDataList = result.data['data'] as List; + // 使用 map 将 List 转换为 List + var stations = stationDataList + .map((item) => StationModel.fromJson(item as Map)) + .toList(); - // 使用 map 将 List 转换为 List - var stations = stationDataList - .map((item) => StationModel.fromJson(item as Map)) - .toList(); + // 去重,确保每个 hydrogenId 唯一 + var uniqueStationsMap = {}; // 使用 Map 来去重 + for (var station in stations) { + uniqueStationsMap[station.hydrogenId] = station; // 使用 hydrogenId 作为键,确保唯一 + } - // 去重,确保每个 hydrogenId 唯一 - var uniqueStationsMap = {}; // 使用 Map 来去重 - for (var station in stations) { - uniqueStationsMap[station.hydrogenId] = station; // 使用 hydrogenId 作为键,确保唯一 - } + // 获取去重后的 List + var uniqueStations = uniqueStationsMap.values.toList(); - // 获取去重后的 List - var uniqueStations = uniqueStationsMap.values.toList(); + stationOptions.assignAll(uniqueStations); - stationOptions.assignAll(uniqueStations); + if (stationOptions.isEmpty) { + showToast('附近暂无可用加氢站'); + } else { + showToast('站点列表已刷新'); + } - if (stationOptions.isEmpty) { - showToast('附近暂无可用加氢站'); - } else { - showToast('站点列表已刷新'); - } - - // 找到第一个可选的站点作为默认值 - if (stationOptions.isNotEmpty) { - final firstSelectable = stationOptions.firstWhere( - (station) => station.isSelect == 1, - orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个 - ); - selectedStationId.value = firstSelectable.hydrogenId; - } else { - // 如果列表为空,确保 selectedStationId 也为空 - selectedStationId.value = null; - } - } catch (e) { - showToast('数据异常'); + // 找到第一个可选的站点作为默认值 + if (stationOptions.isNotEmpty) { + final firstSelectable = stationOptions.firstWhere( + (station) => station.isSelect == 1, + orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个 + ); + selectedStationId.value = firstSelectable.hydrogenId; + } else { + // 如果列表为空,确保 selectedStationId 也为空 + selectedStationId.value = null; } } catch (e) { + dismissLoading(); + showToast('数据异常'); } finally { dismissLoading(); HttpService.to.setBaseUrl(AppTheme.test_service_url); diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index 73422c9..8fe1814 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -181,6 +181,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (result.code != 0) { showToast(result.error); + dismissLoading(); return; } @@ -346,6 +347,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (result.code != 0) { showToast(result.error); + dismissLoading(); return; } From 9ba152b3c34b7b50c2437a851217b8328fa10a74 Mon Sep 17 00:00:00 2001 From: userGyl Date: Mon, 22 Dec 2025 16:15:32 +0800 Subject: [PATCH 09/10] =?UTF-8?q?v1.2.1=20=E5=BE=85=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/ios/Podfile.lock | 17 +- ln_jq_app/lib/pages/qr_code/controller.dart | 219 +++++++------- ln_jq_app/lib/pages/qr_code/view.dart | 313 ++++++++++++-------- ln_jq_app/pubspec.lock | 32 +- ln_jq_app/pubspec.yaml | 3 +- 5 files changed, 312 insertions(+), 272 deletions(-) diff --git a/ln_jq_app/ios/Podfile.lock b/ln_jq_app/ios/Podfile.lock index 799a4c7..bded35d 100644 --- a/ln_jq_app/ios/Podfile.lock +++ b/ln_jq_app/ios/Podfile.lock @@ -20,7 +20,9 @@ PODS: - FlutterMacOS - image_picker_ios (0.0.1): - Flutter - - MTBBarcodeScanner (5.0.11) + - mobile_scanner (7.0.0): + - Flutter + - FlutterMacOS - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter @@ -29,9 +31,6 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - qr_code_scanner_plus (0.2.6): - - Flutter - - MTBBarcodeScanner - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -47,16 +46,15 @@ DEPENDENCIES: - flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - 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`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - - MTBBarcodeScanner - OrderedSet EXTERNAL SOURCES: @@ -76,14 +74,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/geolocator_apple/darwin" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/darwin" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - qr_code_scanner_plus: - :path: ".symlinks/plugins/qr_code_scanner_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: @@ -98,12 +96,11 @@ SPEC CHECKSUMS: flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 - MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b diff --git a/ln_jq_app/lib/pages/qr_code/controller.dart b/ln_jq_app/lib/pages/qr_code/controller.dart index 29b8013..f43f9ac 100644 --- a/ln_jq_app/lib/pages/qr_code/controller.dart +++ b/ln_jq_app/lib/pages/qr_code/controller.dart @@ -1,17 +1,13 @@ import 'dart:io'; -import 'dart:typed_data'; - import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; -import 'package:image/image.dart' as img; 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:qr_code_scanner_plus/qr_code_scanner_plus.dart'; -import 'package:zxing_lib/common.dart'; -import 'package:zxing_lib/qrcode.dart'; -import 'package:zxing_lib/zxing.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:permission_handler/permission_handler.dart'; class QrCodeController extends GetxController with BaseControllerMixin, GetSingleTickerProviderStateMixin { @@ -22,11 +18,11 @@ class QrCodeController extends GetxController late final AnimationController animationController; late final Animation scanAnimation; - // --- QR Scanning --- - final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); - QRViewController? qrViewController; - final Rx result = Rx(null); + // --- 使用 MobileScanner 的控制器 --- + final MobileScannerController scannerController = MobileScannerController(); + final RxBool isFlashOn = false.obs; + final RxBool isProcessingResult = false.obs; @override void onInit() { @@ -37,140 +33,114 @@ class QrCodeController extends GetxController duration: const Duration(milliseconds: 2500), vsync: this, ); - scanAnimation = Tween(begin: 0, end: 1).animate(animationController); + scanAnimation = + Tween(begin: 0, end: 1).animate(animationController); animationController.repeat(reverse: false); } - /// 当 QRView 创建时调用 - void onQRViewCreated(QRViewController controller) { - this.qrViewController = controller; - // 监听扫描到的数据 - controller.scannedDataStream.listen((scanData) { - if (scanData.code != null && result.value?.code != scanData.code) { - result.value = scanData; - qrViewController?.pauseCamera(); + /// MobileScanner 的 onDetect 回调方法 + void onDetect(BarcodeCapture capture) { + if (isProcessingResult.value) return; - animationController.stop(); - - renderResult(scanData.code!); - } - }); + final Barcode? barcode = capture.barcodes.firstOrNull; + if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) { + isProcessingResult.value = true; + scannerController.stop(); + animationController.stop(); + print("相机识别到的内容: ${barcode.rawValue!}"); + renderResult(barcode.rawValue!); + } } + /// 恢复扫描状态 void resumeScanner() { - result.value = null; - qrViewController?.resumeCamera(); + isProcessingResult.value = false; + try { + scannerController.start(); + } catch (e) { + print("无法重启相机: $e"); + } animationController.repeat(reverse: false); } /// 从相册选择图片并扫描二维码 void scanFromGallery() async { try { - final XFile? imageFile = await ImagePicker().pickImage(source: ImageSource.gallery); - if (imageFile == null) return; // 用户取消了选择 - - qrViewController?.pauseCamera(); - 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; + final XFile? imageFile = + await ImagePicker().pickImage(source: ImageSource.gallery); + if (imageFile == null) { + return; } - 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); } else { showErrorToast('未识别到二维码'); resumeScanner(); } - } catch (e) { - showErrorToast('从相册选择失败'); + } catch (e, stackTrace) { + dismissLoading(); + showErrorToast('从相册选择失败,请稍后重试'); + print("scanFromGallery Error: $e\n$stackTrace"); resumeScanner(); } } /// 切换闪光灯 void toggleFlash() async { - await qrViewController?.toggleFlash(); - isFlashOn.value = (await qrViewController?.getFlashStatus()) ?? false; + try { + await scannerController.toggleTorch(); + final currentTorchState = scannerController.value.torchState; + isFlashOn.value = currentTorchState == TorchState.on; + } catch (e) { + print("切换闪光灯失败: $e"); + showErrorToast("无法打开闪光灯"); + } } /// 翻转相机 void flipCamera() async { - await qrViewController?.flipCamera(); + await scannerController.switchCamera(); } + /// 请求相机权限 void requestPermission() async { - if (Platform.isIOS) { - var status = await Permission.camera.request(); - if (status.isGranted) { - } else if (status.isPermanentlyDenied) { - openAppSettings(); - } else { - showErrorToast('需要相机权限才能扫描二维码'); - } - return; - } - - final bool results = await requestCameraPermission(); - if (!results) { - showErrorToast('相机权限未被授予,请到权限管理中打开'); + var status = await Permission.camera.request(); + if (!status.isGranted) { + showErrorToast('请授予相机权限以使用扫描功能'); + Get.back(); } } void requestPhotoPermission() async { - if (Platform.isAndroid) { - final bool results = await requestPhotosPermission(); - if (!results) { - showErrorToast('相册权限未被授予,请到权限管理中打开'); - } else { - scanFromGallery(); - } - } - if (Platform.isIOS) { - var status = await Permission.photos.request(); - print("权限状态: $status"); // 在控制台看这个输出 - if (status.isGranted) { - scanFromGallery(); - } else if (status.isPermanentlyDenied) { - openAppSettings(); - } else { - showErrorToast('需要相册权限才能从相册中选择图片'); - } + var status = await Permission.photos.request(); + if (status.isGranted) { + scanFromGallery(); + } else if (status.isPermanentlyDenied) { + openAppSettings(); + } else { + showErrorToast('需要相册权限才能从相册中选择图片'); } } - //扫码结果处理 //如果绑定接口返回的data为null 需要手动编辑车牌 + /// 处理扫描结果 void renderResult(String resultStr, {plateNumber}) async { showLoading("正在获取车辆信息..."); try { - /*var responseData = await HttpService.to.get( - "appointment/truck/base-info?vin=$resultStr", - );*/ - final Map requestData = { "code": resultStr, "phone": StorageService.to.phone, @@ -184,6 +154,7 @@ class QrCodeController extends GetxController ); if (responseData == null) { + dismissLoading(); showToast('无法获取车辆信息,请检查网络或稍后重试'); resumeScanner(); return; @@ -192,62 +163,76 @@ class QrCodeController extends GetxController 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 (e) { + + } on DioException catch (_) { showErrorToast("网络请求失败,请稍后重试"); resumeScanner(); - } catch (e, stackTrace) { - showErrorToast("处理失败,请稍后重试"); - resumeScanner(); // 未知异常,恢复扫描 + } catch (e, _) { + showErrorToast("处理失败,请稍后重舍"); + resumeScanner(); } finally { - dismissLoading(); + if (Get.isDialogOpen ?? false) { + dismissLoading(); + } } } + /// 显示绑定确认对话框 void showBindDialog(String resultStr) { final TextEditingController plateNumberController = TextEditingController(); - DialogX.to.showNoticeDialog( - icon: DialogIcon.info, + // 使用 showConfirmDialog,它有 onCancel 回调 + DialogX.to.showConfirmDialog( title: '请输入车牌号', barrierDismissible: false, content: TextField( - controller: plateNumberController, // 绑定 controller - autofocus: false, // 弹窗出现时自动获取焦点,方便用户直接输入 + controller: plateNumberController, + autofocus: false, decoration: const InputDecoration( hintText: '请输入完整的车牌号', border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12.0), ), ), - confirmText: '确认', + confirmText: '确认绑定', + cancelText: '取消', // showConfirmDialog 有 cancelText onConfirm: () { final String plateNumber = plateNumberController.text.trim(); if (plateNumber.isEmpty) { - resumeScanner(); showToast("请输入车牌号"); - return; + // 返回 false 可以阻止弹窗关闭,让用户继续输入 + return false; } renderResult(resultStr, plateNumber: plateNumber); + //关闭弹窗 + return true; + }, + onCancel: () { + // 如果用户点击取消,恢复扫描 + resumeScanner(); }, ); } @override void onClose() { - qrViewController?.dispose(); + scannerController.dispose(); animationController.dispose(); super.onClose(); } } + diff --git a/ln_jq_app/lib/pages/qr_code/view.dart b/ln_jq_app/lib/pages/qr_code/view.dart index df58a84..2fe4d59 100644 --- a/ln_jq_app/lib/pages/qr_code/view.dart +++ b/ln_jq_app/lib/pages/qr_code/view.dart @@ -1,148 +1,155 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:getx_scaffold/getx_scaffold.dart'; -import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; +import 'package:ln_jq_app/common/styles/theme.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; import 'controller.dart'; class QrCodePage extends GetView { - const QrCodePage({Key? key}) : super(key: key); + const QrCodePage({super.key}); @override Widget build(BuildContext context) { - return GetBuilder( - init: QrCodeController(), - id: 'qrcode', - builder: (_) { - 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(), + 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)), + ), + ], + ), + 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: [ - _buildQrView(context), - Positioned( - bottom: 80.h, - left: 0, - right: 0, - child: _buildControlButtons(), - ), - ], - ), - ); - }, + // 扫描动画和覆盖层 + _buildScannerOverlay(context), + // 底部的功能按钮 + Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()), + ], + ), ); } - /// 构建二维码扫描视图(带动画) - Widget _buildQrView(BuildContext context) { - // 定义扫描区域的大小 - var scanArea = (MediaQuery.of(context).size.width < 400 || - MediaQuery.of(context).size.height < 400) - ? 250.0 - : 300.0; - + /// 构建扫描区域的覆盖层和动画 + Widget _buildScannerOverlay(BuildContext context) { + // 模拟扫描框的位置和大小 + const double scanAreaSize = 250.0; return Stack( - alignment: Alignment.center, - children: [ - // 底层是相机视图和半透明遮罩 - QRView( - key: controller.qrKey, - onQRViewCreated: controller.onQRViewCreated, - overlay: QrScannerOverlayShape( - borderColor: Colors.blueAccent, - borderRadius: 10, - 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), + 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( - width: scanArea, - height: 2, // 扫描线的高度 + margin: const EdgeInsets.only(bottom: 100), // 微调位置 + width: scanAreaSize, + height: scanAreaSize, decoration: BoxDecoration( - color: Colors.blueAccent, - boxShadow: [ - BoxShadow( - color: Colors.blueAccent.withOpacity(0.7), - blurRadius: 8, - spreadRadius: 2, - ), - ], + color: Colors.black, + borderRadius: BorderRadius.circular(12), ), ), ), - ); - }, + ], + ), + ), + // 扫描动画 + 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() { - 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(() => Icon( - controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off, - color: Colors.white, - size: 28, - )), - ), - // 翻转相机按钮 - _buildIconButton( - onPressed: controller.flipCamera, - child: const Icon( - Icons.flip_camera_ios, - color: Colors.white, - size: 28, + /// 构建底部的功能按钮(闪光灯、相册) + 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, + ), + ), ), - ), - ], - ), - ], - ); - } + // 翻转相机按钮 + _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( @@ -158,3 +165,71 @@ class QrCodePage extends GetView { ); } } + +/// 扫描动画的绘制器 +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; + } +} diff --git a/ln_jq_app/pubspec.lock b/ln_jq_app/pubspec.lock index 018266c..c978e01 100644 --- a/ln_jq_app/pubspec.lock +++ b/ln_jq_app/pubspec.lock @@ -65,14 +65,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: transitive description: @@ -733,6 +725,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: transitive description: @@ -925,14 +925,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: transitive description: @@ -1234,14 +1226,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dart: ">=3.9.0 <4.0.0" flutter: ">=3.35.0" diff --git a/ln_jq_app/pubspec.yaml b/ln_jq_app/pubspec.yaml index 93d5242..9acd578 100644 --- a/ln_jq_app/pubspec.yaml +++ b/ln_jq_app/pubspec.yaml @@ -43,10 +43,9 @@ dependencies: flutter_native_splash: ^2.4.7 dropdown_button2: ^2.3.8 - qr_code_scanner_plus: ^2.0.14 image_picker: ^1.2.1 # 用于从相册选择图片 image: ^4.5.4 - zxing_lib: ^1.1.4 + mobile_scanner: ^7.1.4 flutter_pdfview: 1.4.3 #显示pdf photo_view: ^0.15.0 #操作图片 flutter_inappwebview: ^6.1.5 # WebView插件 From f2f2348b54be3fd50d15809bdb8cc6b9763d7bef Mon Sep 17 00:00:00 2001 From: userGyl Date: Tue, 23 Dec 2025 10:18:44 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E9=80=86=E5=9C=B0=E7=90=86=E7=BC=96?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/assets/html/map.html | 70 ++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/ln_jq_app/assets/html/map.html b/ln_jq_app/assets/html/map.html index 3df0453..5e71f6b 100644 --- a/ln_jq_app/assets/html/map.html +++ b/ln_jq_app/assets/html/map.html @@ -137,7 +137,7 @@ @@ -167,9 +167,11 @@