显示证件照 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: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<String> drivingAttachments = <String>[].obs;
final RxList<String> operationAttachments = <String>[].obs;
final RxList<String> hydrogenationAttachments = <String>[].obs;
final RxList<String> registerAttachments = <String>[].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<String, dynamic>;
List<String> parseAttachments(dynamic list) {
if (list is List) {
// 确保列表中的每一项都是字符串
return List<String>.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<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: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<CarInfoController> {
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<CarInfoController> {
);
}
/// 1. 构建顶部的司机信息卡片(包含蓝色背景)
/// 构建顶部的司机信息卡片
Widget _buildDriverInfoCard() {
return Card(
elevation: 2,
@@ -137,7 +133,7 @@ class CarInfoPage extends GetView<CarInfoController> {
);
}
/// 2. 构建车辆绑定信息卡片
/// 构建车辆绑定信息卡片
Widget _buildCarBindingCard() {
return Card(
elevation: 2,
@@ -226,30 +222,44 @@ class CarInfoPage extends GetView<CarInfoController> {
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<String> attachments,
}) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
@@ -257,24 +267,30 @@ class CarInfoPage extends GetView<CarInfoController> {
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,

View File

@@ -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:

View File

@@ -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: