From fb212fa3865092581713656ae3f3ffe11d01803e Mon Sep 17 00:00:00 2001 From: userGyl Date: Tue, 18 Nov 2025 14:48:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=BE=E7=A4=BA=E8=AF=81=E4=BB=B6=E7=85=A7?= =?UTF-8?q?=20pdf=E5=BE=85=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attachment_viewer_controller.dart | 67 ++++++++ .../car_info/attachment_viewer_page.dart | 80 ++++++++++ .../certificate_viewer_controller.dart | 37 +++++ .../car_info/certificate_viewer_page.dart | 51 ++++++ .../lib/pages/c_page/car_info/controller.dart | 85 +++++----- ln_jq_app/lib/pages/c_page/car_info/view.dart | 146 ++++++++++-------- ln_jq_app/pubspec.lock | 16 ++ ln_jq_app/pubspec.yaml | 3 +- 8 files changed, 383 insertions(+), 102 deletions(-) create mode 100644 ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_controller.dart create mode 100644 ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_page.dart create mode 100644 ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_controller.dart create mode 100644 ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_page.dart 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 new file mode 100644 index 0000000..64b722f --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_controller.dart @@ -0,0 +1,67 @@ +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'; + +class AttachmentViewerController extends GetxController { + late final String url; + late final String fileType; + + final RxBool isLoading = true.obs; + // This is the correct state variable: it stores the local file path. + final RxString localFilePath = ''.obs; + final RxString loadingText = '加载中...'.obs; + + @override + void onInit() { + super.onInit(); + url = Get.arguments['url'] ?? ''; + if (url.isEmpty) { + showErrorToast('无效的附件链接'); + Get.back(); + return; + } + + if (url.toLowerCase().endsWith('.pdf')) { + fileType = 'pdf'; + // This is the correct logic: download the file first. + _downloadPdf(); + } else { + fileType = 'image'; + isLoading.value = false; + } + } + + /// Downloads the PDF file to a temporary directory and stores its path. + Future _downloadPdf() async { + try { + final dio = Dio(); + final tempDir = await getTemporaryDirectory(); + // Use a unique name to avoid caching issues + final fileName = '${DateTime.now().millisecondsSinceEpoch}_${url.split('/').last}'; + final savePath = '${tempDir.path}/$fileName'; + + await dio.download( + url, + savePath, + onReceiveProgress: (received, total) { + if (total != -1) { + loadingText.value = '下载中...${(received / total * 100).toStringAsFixed(0)}%'; + } + }, + ); + + // On success, update the local file path + localFilePath.value = savePath; + + } catch (e) { + showErrorToast('PDF文件加载失败,请检查网络或文件链接'); + print('PDF Download Error: $e'); + Get.back(); + } finally { + isLoading.value = false; + } + } +} 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 new file mode 100644 index 0000000..aaf07b9 --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/car_info/attachment_viewer_page.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pdfview/flutter_pdfview.dart'; +import 'package:get/get.dart'; +import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:photo_view/photo_view.dart'; + +import 'attachment_viewer_controller.dart'; + +class AttachmentViewerPage extends GetView { + const AttachmentViewerPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Get.put(AttachmentViewerController()); + final fileName = controller.url.split('/').last; + + return Scaffold( + appBar: AppBar( + title: Text( + fileName, + style: const TextStyle(fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + centerTitle: true, + ), + body: Obx(() { + if (controller.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 20), + Text(controller.loadingText.value), + ], + ), + ); + } + + 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, + pageFling: true, + onRender: (pages) { + print("PDF rendered with $pages pages."); + }, + onError: (error) { + print(error.toString()); + showErrorToast('渲染PDF时出错'); + }, + onPageError: (page, error) { + print('$page: ${error.toString()}'); + showErrorToast('渲染第$page页时出错'); + }, + ); + } else { + return const Center(child: Text('无法加载PDF文件')); + } + } else { + return PhotoView( + imageProvider: NetworkImage(controller.url), + minScale: PhotoViewComputedScale.contained * 0.8, + maxScale: PhotoViewComputedScale.covered * 2.0, + loadingBuilder: (context, event) => const Center( + child: CircularProgressIndicator(), + ), + backgroundDecoration: const BoxDecoration( + color: Colors.black, + ), + ); + } + }), + ); + } +} 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 new file mode 100644 index 0000000..1ce93eb --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_controller.dart @@ -0,0 +1,37 @@ +import 'package:get/get.dart'; +import 'package:getx_scaffold/getx_scaffold.dart'; + +import 'attachment_viewer_page.dart'; + +class CertificateViewerController extends GetxController { + late final String title; + late final List attachments; + + @override + void onInit() { + super.onInit(); + // 从 Get.to 的 arguments 中获取标题和附件列表 + title = Get.arguments['title'] ?? '证件详情'; + attachments = List.from(Get.arguments['attachments'] ?? []); + } + + /// 导航到通用的附件查看器页面 + void openAttachment(String url) { + if (url.isEmpty) { + showErrorToast('附件链接无效'); + return; + } + + Get.to( + () => const AttachmentViewerPage(), + arguments: { + 'url': url, + }, + ); + } + + /// 检查 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 new file mode 100644 index 0000000..11a88e5 --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/car_info/certificate_viewer_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'certificate_viewer_controller.dart'; + +class CertificateViewerPage extends GetView { + const CertificateViewerPage({Key? key}) : super(key: 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, + ), + title: Text( + fileName, + style: const TextStyle(fontSize: 14), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: Colors.grey), + onTap: () => controller.openAttachment(url), + ), + ); + }, + ), + ); + } +} 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 c294b07..18f0789 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 @@ -1,72 +1,85 @@ +import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.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 'certificate_viewer_page.dart'; + class CarInfoController extends GetxController with BaseControllerMixin { @override String get builderId => 'car_info'; - CarInfoController(); - + // --- 车辆基本信息 --- String plateNumber = ""; String vin = "未知"; String modelName = "未知"; String brandName = "未知"; - @override - void onInit() { - getUserBindCarInfo(); - super.onInit(); - } + // --- 证件附件列表 --- + final RxList drivingAttachments = [].obs; + final RxList operationAttachments = [].obs; + final RxList hydrogenationAttachments = [].obs; + final RxList registerAttachments = [].obs; @override - void onClose() { - super.onClose(); + void onInit() { + super.onInit(); + getUserBindCarInfo(); } void getUserBindCarInfo() async { if (StorageService.to.hasVehicleInfo) { VehicleInfo? bean = StorageService.to.vehicleInfo; - if (bean == null) { - return; - } + if (bean == null) return; + + // 填充基本信息 plateNumber = bean.plateNumber; vin = bean.vin; modelName = bean.modelName; brandName = bean.brandName; + // 获取证件信息 final response = await HttpService.to.get( - 'appointment/vehicle/getPicInfoByVin?vin=${vin}', + 'appointment/vehicle/getPicInfoByVin?vin=$vin', ); - if (response != null) { + + if (response != null && response.data != null) { final result = BaseModel.fromJson(response.data); if (result.code == 0 && result.data != null) { - result.data; - /*{ - "plateNumber": "浙F08860F", - "vin": "LA9GG64L9NBAF4174", - "drivingAttachment": [ - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162107520002.pdf", - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162107530002.jpg", - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/12/202412101043260001.jpg" - ], - "operationAttachment": [ - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162107530003.pdf", - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/12/202412101043460001.jpg", - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/12/202412181551190001.pdf" - ], - "hydrogenationAttachment": [ - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162107570001.pdf" - ], - "registerAttachment": [ - "https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162108010001.pdf" - ] - }*/ + final data = result.data as Map; + + List parseAttachments(dynamic list) { + if (list is List) { + // 确保列表中的每一项都是字符串 + return List.from(list.map((item) => item.toString())); + } + return []; + } + + // 将解析出的 URL 列表赋值给对应的 RxList + drivingAttachments.assignAll(parseAttachments(data['drivingAttachment'])); + operationAttachments.assignAll(parseAttachments(data['operationAttachment'])); + hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment'])); + registerAttachments.assignAll(parseAttachments(data['registerAttachment'])); } } - updateUi(); } } + + /// 跳转到证件查看页面 + void navigateToCertificateViewer(String title, List attachments) { + if (attachments.isEmpty) { + showToast('暂无相关证件附件'); + return; + } + Get.to( + () => const CertificateViewerPage(), + arguments: { + 'title': title, + 'attachments': attachments, + }, + ); + } } diff --git a/ln_jq_app/lib/pages/c_page/car_info/view.dart b/ln_jq_app/lib/pages/c_page/car_info/view.dart index 5a111a4..4dcdf57 100644 --- a/ln_jq_app/lib/pages/c_page/car_info/view.dart +++ b/ln_jq_app/lib/pages/c_page/car_info/view.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/storage_service.dart'; -// import 'package:getx_scaffold/getx_scaffold.dart'; // 如果不使用其中的扩展,可以注释掉 import 'controller.dart'; @@ -17,10 +15,8 @@ class CarInfoPage extends GetView { init: CarInfoController(), id: 'car_info', builder: (_) { - // 将所有 UI 构建逻辑都放在这里 return Scaffold( backgroundColor: Colors.grey[100], - // 我们不再使用单独的 AppBar,而是通过自定义的 Container 来实现类似效果 body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(12.0), @@ -43,7 +39,7 @@ class CarInfoPage extends GetView { ); } - /// 1. 构建顶部的司机信息卡片(包含蓝色背景) + /// 构建顶部的司机信息卡片 Widget _buildDriverInfoCard() { return Card( elevation: 2, @@ -65,13 +61,13 @@ class CarInfoPage extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + Text( "${StorageService.to.name}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), - Text( - "${StorageService.to.phone}", + Text( + "${StorageService.to.phone}", style: TextStyle(color: Colors.grey, fontSize: 11), ), ], @@ -137,7 +133,7 @@ class CarInfoPage extends GetView { ); } - /// 2. 构建车辆绑定信息卡片 + /// 构建车辆绑定信息卡片 Widget _buildCarBindingCard() { return Card( elevation: 2, @@ -176,46 +172,46 @@ class CarInfoPage extends GetView { const SizedBox(width: 8), isButton ? GestureDetector( - onTap: () async { - //判断是否绑定成功 - var scanResult = await Get.to(() => const QrCodePage()); - if (scanResult == true) { - controller.getUserBindCarInfo(); - } - }, - child: Container( - margin: EdgeInsetsGeometry.only(left: 10.w), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), - decoration: BoxDecoration( - border: Border.all(color: Colors.blue.shade300, width: 1), - borderRadius: BorderRadius.circular(5), - color: Colors.blue.withOpacity(0.05), - ), - child: Row( - mainAxisSize: MainAxisSize.min, // Keep the row compact - children: [ - Icon( - StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search, - size: 13, - color: Colors.blue, - ), - const SizedBox(width: 3), - Text( - StorageService.to.hasVehicleInfo ? "换车牌" : value, - style: const TextStyle( - color: Colors.blue, - fontSize: 11, - fontWeight: FontWeight.w500, + onTap: () async { + //判断是否绑定成功 + var scanResult = await Get.to(() => const QrCodePage()); + if (scanResult == true) { + controller.getUserBindCarInfo(); + } + }, + child: Container( + margin: EdgeInsetsGeometry.only(left: 10.w), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + decoration: BoxDecoration( + border: Border.all(color: Colors.blue.shade300, width: 1), + borderRadius: BorderRadius.circular(5), + color: Colors.blue.withOpacity(0.05), + ), + child: Row( + mainAxisSize: MainAxisSize.min, // Keep the row compact + children: [ + Icon( + StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search, + size: 13, + color: Colors.blue, + ), + const SizedBox(width: 3), + Text( + StorageService.to.hasVehicleInfo ? "换车牌" : value, + style: const TextStyle( + color: Colors.blue, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - ], - ), - ), - ) + ) : Text( - value, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), - ), + value, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), ], ); } @@ -226,30 +222,44 @@ class CarInfoPage extends GetView { elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '车辆证件', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + _buildCertificateRow( + icon: Icons.credit_card_rounded, + title: '行驶证', + attachments: controller.drivingAttachments, ), - const SizedBox(height: 8), - _buildCertificateRow(Icons.local_gas_station, '驾驶证', '车辆驾驶证相关证件'), const Divider(), - _buildCertificateRow(Icons.article_outlined, '营运证', '道路运输经营许可证'), + _buildCertificateRow( + icon: Icons.article_rounded, + title: '运营证', + attachments: controller.operationAttachments, + ), const Divider(), - _buildCertificateRow(Icons.person_pin_outlined, '加氢证', '车辆加氢许可证'), + _buildCertificateRow( + icon: Icons.propane_tank_rounded, + title: '氢瓶证', + attachments: controller.hydrogenationAttachments, + ), const Divider(), - _buildCertificateRow(Icons.credit_card_outlined, '行驶证', '车辆行驶证相关证件'), + _buildCertificateRow( + icon: Icons.app_registration_rounded, + title: '登记证', + attachments: controller.registerAttachments, + ), ], ), ), ); } - // 车辆证件列表项 - Widget _buildCertificateRow(IconData icon, String title, String subtitle) { + /// 证件展示 + Widget _buildCertificateRow({ + required IconData icon, + required String title, + required List attachments, + }) { return ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( @@ -257,24 +267,30 @@ class CarInfoPage extends GetView { backgroundColor: Colors.blue.withOpacity(0.1), child: Icon(icon, color: Colors.blue, size: 28), ), - title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold,fontSize: 14)), - subtitle: Text(subtitle, style: const TextStyle(color: Colors.grey, fontSize: 12)), + title: Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + // 使用 Obx 响应式地显示附件数量 + subtitle: Obx( + () => Text( + '共 ${attachments.length} 个附件', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), trailing: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(8), ), - // 图片中的图标是“查看”的意思,这里用一个类似的图标代替 child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor), ), - onTap: () { - // TODO: 查看证件详情逻辑 - }, + // 更新 onTap 逻辑 + onTap: () => controller.navigateToCertificateViewer(title, attachments), ); } - /// 4. 构建提示信息卡片 Widget _buildTipsCard() { return Card( elevation: 2, diff --git a/ln_jq_app/pubspec.lock b/ln_jq_app/pubspec.lock index c8cda99..bf7a574 100644 --- a/ln_jq_app/pubspec.lock +++ b/ln_jq_app/pubspec.lock @@ -323,6 +323,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.7" + flutter_pdfview: + dependency: "direct main" + description: + name: flutter_pdfview + sha256: a9055bf920c7095bf08c2781db431ba23577aa5da5a056a7152dc89a18fbec6f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -733,6 +741,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.0" platform: dependency: transitive description: diff --git a/ln_jq_app/pubspec.yaml b/ln_jq_app/pubspec.yaml index b1f101e..b2f240f 100644 --- a/ln_jq_app/pubspec.yaml +++ b/ln_jq_app/pubspec.yaml @@ -47,7 +47,8 @@ dependencies: image_picker: ^1.2.1 # 用于从相册选择图片 image: ^4.5.4 zxing_lib: ^1.1.4 - + flutter_pdfview: 1.3.2 #显示pdf + photo_view: ^0.15.0 #操作图片 dev_dependencies: