车辆信息 ui

This commit is contained in:
2026-01-23 15:02:16 +08:00
parent aabfbfae0c
commit 16bae6a1e9
4 changed files with 293 additions and 102 deletions

View File

@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(AttachmentViewerController()); Get.put(AttachmentViewerController());
final fileName = controller.url.split('/').last; // final fileName = controller.url.split('/').last;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
fileName, "证件详情",
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart'; import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController with BaseControllerMixin{ class CertificateViewerController extends GetxController with BaseControllerMixin {
late final String title; late final String title;
late final List<String> attachments; late final List<String> attachments;
@@ -78,18 +78,11 @@ class CertificateViewerController extends GetxController with BaseControllerMixi
return; return;
} }
Get.to( Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
() => const AttachmentViewerPage(),
arguments: {
'url': url,
},
);
} }
/// 检查 URL 是否为 PDF (此方法保持不变) /// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) { bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf'); return url.toLowerCase().endsWith('.pdf');
} }
} }

View File

@@ -2,10 +2,12 @@ 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/pages/c_page/car_info/attachment_viewer_page.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:path_provider/path_provider.dart';
import 'certificate_viewer_page.dart'; import 'certificate_viewer_page.dart';
import 'dart:io';
class CarInfoController extends GetxController with BaseControllerMixin { class CarInfoController extends GetxController with BaseControllerMixin {
@override @override
@@ -117,6 +119,15 @@ class CarInfoController extends GetxController with BaseControllerMixin {
parseAttachments(data['hydrogenationAttachment']), parseAttachments(data['hydrogenationAttachment']),
); );
registerAttachments.assignAll(parseAttachments(data['registerAttachment'])); registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
// 初始化时开始加载所有PDF
attachments = [
...drivingAttachments,
...operationAttachments,
...hydrogenationAttachments,
...registerAttachments,
];
loadAllPdfs();
} }
} }
updateUi(); updateUi();
@@ -138,4 +149,69 @@ class CarInfoController extends GetxController with BaseControllerMixin {
arguments: {'title': title, 'attachments': attachments}, 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<String> attachments = [];
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
/// 遍历所有附件如果是PDF则进行下载
void loadAllPdfs() {
for (var url in attachments) {
if (isPdf(url)) {
_downloadPdf(url);
}
}
}
/// 下载单个PDF文件
Future<void> _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;
}
}
} }

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.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/login_util.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/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:photo_view/photo_view.dart';
import '../../../common/styles/theme.dart'; import '../../../common/styles/theme.dart';
import 'controller.dart'; import 'controller.dart';
@@ -18,21 +20,20 @@ class CarInfoPage extends GetView<CarInfoController> {
id: 'car_info', id: 'car_info',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: Color.fromRGBO(240, 244, 247, 0.4), backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
_buildUserInfoCard(), _buildUserInfoCard(),
Padding( Padding(
padding: const EdgeInsets.all(12.0), padding: EdgeInsets.only(left: 20.w,right: 20.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 5), const SizedBox(height: 16),
_buildCarInfoCard(), _buildCarInfoCard(),
const SizedBox(height: 5), _buildCertificatesCard(context),
_buildCertificatesCard(), const SizedBox(height: 12),
const SizedBox(height: 5),
_buildSafetyReminderCard(), _buildSafetyReminderCard(),
], ],
), ),
@@ -60,7 +61,6 @@ class CarInfoPage extends GetView<CarInfoController> {
children: [ children: [
Padding( Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40),
// 增加了顶部 padding 适配状态栏
child: Row( child: Row(
children: [ children: [
Stack( Stack(
@@ -102,15 +102,14 @@ class CarInfoPage extends GetView<CarInfoController> {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景 color: const Color.fromRGBO(236, 255, 234, 1),
border: Border.all(color: const Color(0xFFB7E19F)), // 边框 border: Border.all(color: const Color(0xFFB7E19F)),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)), Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
// 叶子图标
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
"绿色先锋", "绿色先锋",
@@ -173,7 +172,6 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
// 统计项
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) { Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Expanded( return Expanded(
child: Container( child: Container(
@@ -216,7 +214,6 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
/// 构建车辆信息卡片
Widget _buildCarInfoCard() { Widget _buildCarInfoCard() {
return Card( return Card(
elevation: 2, elevation: 2,
@@ -227,13 +224,13 @@ class CarInfoPage extends GetView<CarInfoController> {
child: Column( child: Column(
children: [ children: [
_buildDetailRow('车牌号', controller.plateNumber, isPlate: true), _buildDetailRow('车牌号', controller.plateNumber, isPlate: true),
const SizedBox(height: 12), const SizedBox(height: 11),
_buildDetailRow('车架号', controller.vin), _buildDetailRow('车架号', controller.vin),
const SizedBox(height: 12), const SizedBox(height: 11),
_buildDetailRow('车辆型号', controller.modelName), _buildDetailRow('车辆型号', controller.modelName),
const SizedBox(height: 12), const SizedBox(height: 11),
_buildDetailRow('车辆品牌', controller.brandName), _buildDetailRow('车辆品牌', controller.brandName),
const SizedBox(height: 24), const SizedBox(height: 10),
_buildH2LevelProgress(), _buildH2LevelProgress(),
], ],
), ),
@@ -241,7 +238,6 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
/// 详情行:左右分布
Widget _buildDetailRow(String label, String value, {bool isPlate = false}) { Widget _buildDetailRow(String label, String value, {bool isPlate = false}) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -257,17 +253,21 @@ class CarInfoPage extends GetView<CarInfoController> {
margin: const EdgeInsets.only(right: 10), margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration( 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), borderRadius: BorderRadius.circular(12),
color: Color.fromRGBO(235, 250, 255, 1), // 极浅绿色背景 color: const Color.fromRGBO(235, 250, 255, 1),
), ),
child: Row( child: Row(
children: [ children: [
Icon(Icons.sync, size: 12, color: Color.fromRGBO(71, 174, 208, 1)), const Icon(
SizedBox(width: 4), Icons.sync,
size: 12,
color: Color.fromRGBO(71, 174, 208, 1),
),
const SizedBox(width: 4),
Text( Text(
StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定", StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定",
style: TextStyle( style: const TextStyle(
color: Color.fromRGBO(71, 174, 208, 1), color: Color.fromRGBO(71, 174, 208, 1),
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -291,17 +291,16 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
/// H2 Level 进度条模块
Widget _buildH2LevelProgress() { Widget _buildH2LevelProgress() {
return Column( return Column(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: const LinearProgressIndicator( child: const LinearProgressIndicator(
value: 0.75, // 示例值 value: 0.75,
minHeight: 8, minHeight: 8,
backgroundColor: Color(0xFFF0F2F5), backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF52C41A)), valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -313,7 +312,7 @@ class CarInfoPage extends GetView<CarInfoController> {
"75%", "75%",
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Color(0xFF52C41A), color: Color.fromRGBO(16, 185, 129, 1),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -323,76 +322,195 @@ class CarInfoPage extends GetView<CarInfoController> {
); );
} }
/// 3. 构建车辆证件卡片 /// 3. 构建车辆证件卡片 (重构为 TabView 样式)
Widget _buildCertificatesCard() { Widget _buildCertificatesCard(BuildContext context) {
return Card( return DefaultTabController(
elevation: 2, length: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Column(
child: Padding( children: [
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), TabBar(
child: Column( isScrollable: false,
children: [ indicatorColor: Color.fromRGBO(16, 185, 129, 1),
_buildCertificateRow( labelColor: Color.fromRGBO(16, 185, 129, 1),
icon: Icons.credit_card_rounded, unselectedLabelColor: Colors.grey,
title: '行驶证', labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
attachments: controller.drivingAttachments, 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({ Widget _buildCertificateContent(String title, RxList<String> attachments) {
required IconData icon, return Obx(() {
required String title, if (attachments.isEmpty) {
required List<String> attachments, 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( return Container(
contentPadding: EdgeInsets.zero, width: isFull ? null : 140,
leading: CircleAvatar( child: Column(
radius: 24, crossAxisAlignment: CrossAxisAlignment.start,
backgroundColor: Colors.blue.withOpacity(0.1), children: [
child: Icon(icon, color: Colors.blue, size: 28), 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),
), /// 附件预览组件 (智能判断 PDF 或图片)
subtitle: Obx( Widget _buildAttachmentPreview(String url) {
() => Text( return AspectRatio(
'${attachments.length} 个附件', aspectRatio: 4 / 3,
style: const TextStyle(color: Colors.grey, fontSize: 12), child: controller.isPdf(url)
), ? Obx(() {
), final bool loading = controller.isLoading[url] ?? true;
trailing: Container( final String? localPath = controller.localPdfPaths[url];
padding: const EdgeInsets.all(8),
decoration: BoxDecoration( if (loading) {
color: Colors.grey[200], return _buildLoadingIndicator();
borderRadius: BorderRadius.circular(8), } else if (localPath != null && localPath.isNotEmpty) {
), return IgnorePointer(
child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor), ignoring: true, // 设置为 true 来忽略所有指针事件
), child: PDFView(
onTap: () => controller.navigateToCertificateViewer(title, attachments), 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<CarInfoController> {
const SizedBox(height: 12), const SizedBox(height: 12),
const Text( 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 SizedBox(height: 8),
const Text( const Text(