显示证件照 pdf待测试

This commit is contained in:
2025-11-18 14:48:05 +08:00
parent 3a9efab8ad
commit fb212fa386
8 changed files with 383 additions and 102 deletions

View File

@@ -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<void> _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;
}
}
}

View File

@@ -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<AttachmentViewerController> {
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,
),
);
}
}),
);
}
}

View File

@@ -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<String> attachments;
@override
void onInit() {
super.onInit();
// 从 Get.to 的 arguments 中获取标题和附件列表
title = Get.arguments['title'] ?? '证件详情';
attachments = List<String>.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');
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'certificate_viewer_controller.dart';
class CertificateViewerPage extends GetView<CertificateViewerController> {
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),
),
);
},
),
);
}
}

View File

@@ -1,72 +1,85 @@
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.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/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart'; import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'certificate_viewer_page.dart';
class CarInfoController extends GetxController with BaseControllerMixin { class CarInfoController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'car_info'; String get builderId => 'car_info';
CarInfoController(); // --- 车辆基本信息 ---
String plateNumber = ""; String plateNumber = "";
String vin = "未知"; String vin = "未知";
String modelName = "未知"; String modelName = "未知";
String brandName = "未知"; String brandName = "未知";
@override // --- 证件附件列表 ---
void onInit() { final RxList<String> drivingAttachments = <String>[].obs;
getUserBindCarInfo(); final RxList<String> operationAttachments = <String>[].obs;
super.onInit(); final RxList<String> hydrogenationAttachments = <String>[].obs;
} final RxList<String> registerAttachments = <String>[].obs;
@override @override
void onClose() { void onInit() {
super.onClose(); super.onInit();
getUserBindCarInfo();
} }
void getUserBindCarInfo() async { void getUserBindCarInfo() async {
if (StorageService.to.hasVehicleInfo) { if (StorageService.to.hasVehicleInfo) {
VehicleInfo? bean = StorageService.to.vehicleInfo; VehicleInfo? bean = StorageService.to.vehicleInfo;
if (bean == null) { if (bean == null) return;
return;
} // 填充基本信息
plateNumber = bean.plateNumber; plateNumber = bean.plateNumber;
vin = bean.vin; vin = bean.vin;
modelName = bean.modelName; modelName = bean.modelName;
brandName = bean.brandName; brandName = bean.brandName;
// 获取证件信息
final response = await HttpService.to.get( 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); final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) { if (result.code == 0 && result.data != null) {
result.data; final data = result.data as Map<String, dynamic>;
/*{
"plateNumber": "浙F08860F", List<String> parseAttachments(dynamic list) {
"vin": "LA9GG64L9NBAF4174", if (list is List) {
"drivingAttachment": [ // 确保列表中的每一项都是字符串
"https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162107520002.pdf", return List<String>.from(list.map((item) => item.toString()));
"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" return [];
], }
"operationAttachment": [
"https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/05/202405162107530003.pdf", // 将解析出的 URL 列表赋值给对应的 RxList
"https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/12/202412101043460001.jpg", drivingAttachments.assignAll(parseAttachments(data['drivingAttachment']));
"https://lnh2e.com/api/lingniu-export-v1/v1/resource/file/2024/12/202412181551190001.pdf" operationAttachments.assignAll(parseAttachments(data['operationAttachment']));
], hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment']));
"hydrogenationAttachment": [ registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
"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"
]
}*/
} }
} }
updateUi(); updateUi();
} }
} }
/// 跳转到证件查看页面
void navigateToCertificateViewer(String title, List<String> attachments) {
if (attachments.isEmpty) {
showToast('暂无相关证件附件');
return;
}
Get.to(
() => const CertificateViewerPage(),
arguments: {
'title': title,
'attachments': attachments,
},
);
}
} }

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/styles/theme.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/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
// import 'package:getx_scaffold/getx_scaffold.dart'; // 如果不使用其中的扩展,可以注释掉
import 'controller.dart'; import 'controller.dart';
@@ -17,10 +15,8 @@ class CarInfoPage extends GetView<CarInfoController> {
init: CarInfoController(), init: CarInfoController(),
id: 'car_info', id: 'car_info',
builder: (_) { builder: (_) {
// 将所有 UI 构建逻辑都放在这里
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
// 我们不再使用单独的 AppBar而是通过自定义的 Container 来实现类似效果
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
@@ -43,7 +39,7 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
/// 1. 构建顶部的司机信息卡片(包含蓝色背景) /// 构建顶部的司机信息卡片
Widget _buildDriverInfoCard() { Widget _buildDriverInfoCard() {
return Card( return Card(
elevation: 2, elevation: 2,
@@ -65,13 +61,13 @@ class CarInfoPage extends GetView<CarInfoController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"${StorageService.to.name}", "${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
"${StorageService.to.phone}", "${StorageService.to.phone}",
style: TextStyle(color: Colors.grey, fontSize: 11), style: TextStyle(color: Colors.grey, fontSize: 11),
), ),
], ],
@@ -137,7 +133,7 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
/// 2. 构建车辆绑定信息卡片 /// 构建车辆绑定信息卡片
Widget _buildCarBindingCard() { Widget _buildCarBindingCard() {
return Card( return Card(
elevation: 2, elevation: 2,
@@ -176,46 +172,46 @@ class CarInfoPage extends GetView<CarInfoController> {
const SizedBox(width: 8), const SizedBox(width: 8),
isButton isButton
? GestureDetector( ? GestureDetector(
onTap: () async { onTap: () async {
//判断是否绑定成功 //判断是否绑定成功
var scanResult = await Get.to(() => const QrCodePage()); var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) { if (scanResult == true) {
controller.getUserBindCarInfo(); controller.getUserBindCarInfo();
} }
}, },
child: Container( child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w), margin: EdgeInsetsGeometry.only(left: 10.w),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.blue.shade300, width: 1), border: Border.all(color: Colors.blue.shade300, width: 1),
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
color: Colors.blue.withOpacity(0.05), color: Colors.blue.withOpacity(0.05),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, // Keep the row compact mainAxisSize: MainAxisSize.min, // Keep the row compact
children: [ children: [
Icon( Icon(
StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search, StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search,
size: 13, size: 13,
color: Colors.blue, color: Colors.blue,
), ),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
StorageService.to.hasVehicleInfo ? "换车牌" : value, StorageService.to.hasVehicleInfo ? "换车牌" : value,
style: const TextStyle( style: const TextStyle(
color: Colors.blue, color: Colors.blue,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
),
),
],
), ),
), ),
], )
),
),
)
: Text( : Text(
value, value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
), ),
], ],
); );
} }
@@ -226,30 +222,44 @@ class CarInfoPage extends GetView<CarInfoController> {
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( _buildCertificateRow(
'车辆证件', icon: Icons.credit_card_rounded,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), title: '行驶证',
attachments: controller.drivingAttachments,
), ),
const SizedBox(height: 8),
_buildCertificateRow(Icons.local_gas_station, '驾驶证', '车辆驾驶证相关证件'),
const Divider(), const Divider(),
_buildCertificateRow(Icons.article_outlined, '营运证', '道路运输经营许可证'), _buildCertificateRow(
icon: Icons.article_rounded,
title: '运营证',
attachments: controller.operationAttachments,
),
const Divider(), const Divider(),
_buildCertificateRow(Icons.person_pin_outlined, '加氢证', '车辆加氢许可证'), _buildCertificateRow(
icon: Icons.propane_tank_rounded,
title: '氢瓶证',
attachments: controller.hydrogenationAttachments,
),
const Divider(), 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<String> attachments,
}) {
return ListTile( return ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: CircleAvatar( leading: CircleAvatar(
@@ -257,24 +267,30 @@ class CarInfoPage extends GetView<CarInfoController> {
backgroundColor: Colors.blue.withOpacity(0.1), backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 28), child: Icon(icon, color: Colors.blue, size: 28),
), ),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold,fontSize: 14)), title: Text(
subtitle: Text(subtitle, style: const TextStyle(color: Colors.grey, fontSize: 12)), 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( trailing: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
// 图片中的图标是“查看”的意思,这里用一个类似的图标代替
child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor), child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor),
), ),
onTap: () { // 更新 onTap 逻辑
// TODO: 查看证件详情逻辑 onTap: () => controller.navigateToCertificateViewer(title, attachments),
},
); );
} }
/// 4. 构建提示信息卡片
Widget _buildTipsCard() { Widget _buildTipsCard() {
return Card( return Card(
elevation: 2, elevation: 2,

View File

@@ -323,6 +323,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.7" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -733,6 +741,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" 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: platform:
dependency: transitive dependency: transitive
description: description:

View File

@@ -47,7 +47,8 @@ dependencies:
image_picker: ^1.2.1 # 用于从相册选择图片 image_picker: ^1.2.1 # 用于从相册选择图片
image: ^4.5.4 image: ^4.5.4
zxing_lib: ^1.1.4 zxing_lib: ^1.1.4
flutter_pdfview: 1.3.2 #显示pdf
photo_view: ^0.15.0 #操作图片
dev_dependencies: dev_dependencies: