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 666288c..40f24da 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 @@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView { @override Widget build(BuildContext context) { Get.put(AttachmentViewerController()); - final fileName = controller.url.split('/').last; + // final fileName = controller.url.split('/').last; return Scaffold( appBar: AppBar( title: Text( - fileName, + "证件详情", style: const TextStyle(fontSize: 16), overflow: TextOverflow.ellipsis, ), 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 b431d90..215ac78 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 @@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart'; import 'attachment_viewer_page.dart'; -class CertificateViewerController extends GetxController with BaseControllerMixin{ +class CertificateViewerController extends GetxController with BaseControllerMixin { late final String title; late final List attachments; @@ -78,18 +78,11 @@ class CertificateViewerController extends GetxController with BaseControllerMixi return; } - Get.to( - () => const AttachmentViewerPage(), - arguments: { - 'url': url, - }, - ); + 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/controller.dart b/ln_jq_app/lib/pages/c_page/car_info/controller.dart index 22a7652..e0cabf2 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 @@ -2,10 +2,12 @@ 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/pages/c_page/car_info/attachment_viewer_page.dart'; import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/storage_service.dart'; - +import 'package:path_provider/path_provider.dart'; import 'certificate_viewer_page.dart'; +import 'dart:io'; class CarInfoController extends GetxController with BaseControllerMixin { @override @@ -117,6 +119,15 @@ class CarInfoController extends GetxController with BaseControllerMixin { parseAttachments(data['hydrogenationAttachment']), ); registerAttachments.assignAll(parseAttachments(data['registerAttachment'])); + + // 初始化时开始加载所有PDF + attachments = [ + ...drivingAttachments, + ...operationAttachments, + ...hydrogenationAttachments, + ...registerAttachments, + ]; + loadAllPdfs(); } } updateUi(); @@ -138,4 +149,69 @@ class CarInfoController extends GetxController with BaseControllerMixin { arguments: {'title': title, 'attachments': 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'); + } + + List attachments = []; + + // --- 新增: 状态管理 --- + /// 用于存储网络PDF的本地路径,key是网络url,value是本地路径 + final RxMap localPdfPaths = {}.obs; + + /// 用于跟踪每个附件的加载状态,key是网络url + final RxMap isLoading = {}.obs; + + /// 遍历所有附件,如果是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; + } + } } 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 38e2bf5..d0ca30f 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,9 +1,11 @@ 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:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart'; import 'package:ln_jq_app/storage_service.dart'; +import 'package:photo_view/photo_view.dart'; import '../../../common/styles/theme.dart'; import 'controller.dart'; @@ -18,21 +20,20 @@ class CarInfoPage extends GetView { id: 'car_info', builder: (_) { return Scaffold( - backgroundColor: Color.fromRGBO(240, 244, 247, 0.4), + backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4), body: SingleChildScrollView( child: Column( children: [ _buildUserInfoCard(), Padding( - padding: const EdgeInsets.all(12.0), + padding: EdgeInsets.only(left: 20.w,right: 20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 5), + const SizedBox(height: 16), _buildCarInfoCard(), - const SizedBox(height: 5), - _buildCertificatesCard(), - const SizedBox(height: 5), + _buildCertificatesCard(context), + const SizedBox(height: 12), _buildSafetyReminderCard(), ], ), @@ -60,7 +61,6 @@ class CarInfoPage extends GetView { children: [ Padding( padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40), - // 增加了顶部 padding 适配状态栏 child: Row( children: [ Stack( @@ -102,15 +102,14 @@ class CarInfoPage extends GetView { vertical: 2, ), decoration: BoxDecoration( - color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景 - border: Border.all(color: const Color(0xFFB7E19F)), // 边框 + color: const Color.fromRGBO(236, 255, 234, 1), + border: Border.all(color: const Color(0xFFB7E19F)), borderRadius: BorderRadius.circular(12), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)), - // 叶子图标 SizedBox(width: 4), Text( "绿色先锋", @@ -173,7 +172,6 @@ class CarInfoPage extends GetView { ); } - // 统计项 Widget _buildModernStatItem(String title, String subtitle, String value, String unit) { return Expanded( child: Container( @@ -216,7 +214,6 @@ class CarInfoPage extends GetView { ); } - /// 构建车辆信息卡片 Widget _buildCarInfoCard() { return Card( elevation: 2, @@ -227,13 +224,13 @@ class CarInfoPage extends GetView { child: Column( children: [ _buildDetailRow('车牌号', controller.plateNumber, isPlate: true), - const SizedBox(height: 12), + const SizedBox(height: 11), _buildDetailRow('车架号', controller.vin), - const SizedBox(height: 12), + const SizedBox(height: 11), _buildDetailRow('车辆型号', controller.modelName), - const SizedBox(height: 12), + const SizedBox(height: 11), _buildDetailRow('车辆品牌', controller.brandName), - const SizedBox(height: 24), + const SizedBox(height: 10), _buildH2LevelProgress(), ], ), @@ -241,7 +238,6 @@ class CarInfoPage extends GetView { ); } - /// 详情行:左右分布 Widget _buildDetailRow(String label, String value, {bool isPlate = false}) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -257,17 +253,21 @@ class CarInfoPage extends GetView { margin: const EdgeInsets.only(right: 10), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - border: Border.all(color: Color.fromRGBO(71, 174, 208, 1)), // 浅绿色边框 + border: Border.all(color: const Color.fromRGBO(71, 174, 208, 1)), borderRadius: BorderRadius.circular(12), - color: Color.fromRGBO(235, 250, 255, 1), // 极浅绿色背景 + color: const Color.fromRGBO(235, 250, 255, 1), ), child: Row( children: [ - Icon(Icons.sync, size: 12, color: Color.fromRGBO(71, 174, 208, 1)), - SizedBox(width: 4), + const Icon( + Icons.sync, + size: 12, + color: Color.fromRGBO(71, 174, 208, 1), + ), + const SizedBox(width: 4), Text( StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定", - style: TextStyle( + style: const TextStyle( color: Color.fromRGBO(71, 174, 208, 1), fontSize: 10, fontWeight: FontWeight.bold, @@ -291,17 +291,16 @@ class CarInfoPage extends GetView { ); } - /// H2 Level 进度条模块 Widget _buildH2LevelProgress() { return Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(4), child: const LinearProgressIndicator( - value: 0.75, // 示例值 + value: 0.75, minHeight: 8, backgroundColor: Color(0xFFF0F2F5), - valueColor: AlwaysStoppedAnimation(Color(0xFF52C41A)), + valueColor: AlwaysStoppedAnimation(Color.fromRGBO(16, 185, 129, 1)), ), ), const SizedBox(height: 8), @@ -313,7 +312,7 @@ class CarInfoPage extends GetView { "75%", style: TextStyle( fontSize: 11, - color: Color(0xFF52C41A), + color: Color.fromRGBO(16, 185, 129, 1), fontWeight: FontWeight.bold, ), ), @@ -323,76 +322,195 @@ class CarInfoPage extends GetView { ); } - /// 3. 构建车辆证件卡片 - Widget _buildCertificatesCard() { - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Column( - children: [ - _buildCertificateRow( - icon: Icons.credit_card_rounded, - title: '行驶证', - attachments: controller.drivingAttachments, + /// 3. 构建车辆证件卡片 (重构为 TabView 样式) + Widget _buildCertificatesCard(BuildContext context) { + return DefaultTabController( + length: 4, + child: Column( + children: [ + TabBar( + isScrollable: false, + indicatorColor: Color.fromRGBO(16, 185, 129, 1), + labelColor: Color.fromRGBO(16, 185, 129, 1), + unselectedLabelColor: Colors.grey, + labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + indicatorSize: TabBarIndicatorSize.label, + tabs: const [ + Tab(text: '行驶证'), + Tab(text: '营运证'), + Tab(text: '加氢资格证'), + Tab(text: '登记证'), + ], + ), + const SizedBox(height: 9), + SizedBox( + height: 343.h, // 给定一个高度,或者使用别的方式布局 + child: TabBarView( + children: [ + _buildCertificateContent('行驶证', controller.drivingAttachments), + _buildCertificateContent('营运证', controller.operationAttachments), + _buildCertificateContent('加氢资格证', controller.hydrogenationAttachments), + _buildCertificateContent('登记证', controller.registerAttachments), + ], ), - const Divider(), - _buildCertificateRow( - icon: Icons.article_rounded, - title: '运营证', - attachments: controller.operationAttachments, - ), - const Divider(), - _buildCertificateRow( - icon: Icons.propane_tank_rounded, - title: '加氢证', - attachments: controller.hydrogenationAttachments, - ), - const Divider(), - _buildCertificateRow( - icon: Icons.app_registration_rounded, - title: '登记证', - attachments: controller.registerAttachments, - ), - ], - ), + ), + ], ), ); } - /// 证件展示 - Widget _buildCertificateRow({ - required IconData icon, - required String title, - required List attachments, + /// 构建单个证件的展示内容 + Widget _buildCertificateContent(String title, RxList attachments) { + return Obx(() { + if (attachments.isEmpty) { + return const Center(child: Text('暂无相关证件信息')); + } + return Card( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildCertDetailItem('所有人', '上海羚牛氢运物联网科技有限公司', isFull: true), + _buildCertDetailItem('车辆识别代号', controller.vin), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildCertDetailItem( + '有效期至', + '2028-08-14', + valueColor: const Color(0xFF52C41A), + ), + _buildCertDetailItem('使用性质', '货运'), + ], + ), + const SizedBox(height: 20), + // 附件预览部分 + Expanded( + child: ListView.builder( + scrollDirection: Axis.vertical, + itemCount: attachments.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final url = attachments[index]; + return GestureDetector( + onTap: () { + controller.openAttachment(url); + }, + child: Container( + height: 184.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Color.fromRGBO(226, 232, 240, 1)), + color: Color.fromRGBO(248, 250, 252, 1) + ), + child: Center(child: _buildAttachmentPreview(url)), + ), + ); + }, + ), + ), + ], + ), + ), + ); + }); + } + + Widget _buildCertDetailItem( + String label, + String value, { + Color? valueColor, + bool isFull = false, }) { - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - radius: 24, - backgroundColor: Colors.blue.withOpacity(0.1), - child: Icon(icon, color: Colors.blue, size: 28), + return Container( + width: isFull ? null : 140, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: valueColor ?? Colors.black87, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], ), - title: Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - 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: () => controller.navigateToCertificateViewer(title, attachments), + ); + } + + /// 附件预览组件 (智能判断 PDF 或图片) + Widget _buildAttachmentPreview(String url) { + return 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( + backgroundColor: Color.fromRGBO(248, 250, 252, 1), + 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 _buildLoadingIndicator() { + return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + } + + Widget _buildErrorIndicator() { + return const SizedBox( + height: 200, + child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)), ); } @@ -424,7 +542,11 @@ class CarInfoPage extends GetView { const SizedBox(height: 12), const Text( "请确保车辆证件齐全有效,定期检查车辆状态和证件有效期,以确保运输作业合规安全。", - style: TextStyle(fontSize: 13, color: Color.fromRGBO(1, 113, 55, 0.8), height: 1.5), + style: TextStyle( + fontSize: 13, + color: Color.fromRGBO(1, 113, 55, 0.8), + height: 1.5, + ), ), const SizedBox(height: 8), const Text(